Skip to content
Open Ontology

Open Ontology · Configuration as code for every platform

Configuration for every platform, checked as one program.

Salesforce objects, Stripe prices, Okta groups, and GitHub teams already depend on one another. Today those dependencies are implicit: copied by hand between dashboards, and verified by no one.

Open Ontology describes each platform with a Forma prelude that elaborates to its native configuration. Preludes share a standard library of domain types, so every reference between projects is type-checked before anything is deployed.

MIT licensed Standard library included Source ↗ Discord ↗

↑ a scripted session of the v0.2 design · what runs today
Any platformOne prelude per platformIn that platform's own words
Typed across projectsReferences between platforms are checkedRename once, see every break
Shared vocabularyA standard library of domain typesPerson, Organization, Team, Money
Native outputElaborates to real configMetadata XML, API payloads, ontology IR

00The hidden graph

Your platforms already depend on each other. Nothing records it.

In almost any company, configuration in one system quietly depends on configuration in another. Copied ids, mirrored picklists, groups that must match teams. That graph is your company's real ontology. Open Ontology makes it typed.

  1. stripecustomer.metadata.sf_account_id← a Salesforce Account Id, pasted by handunchecked
  2. salesforceOpportunity.Plan__c (picklist)← a copy of Stripe's lookup keysunchecked
  3. oktaapp assignment → "Sales User"← a Salesforce profile nameunchecked
  4. githubteam @acme/billing-eng← should match an Okta groupunchecked
  5. pagerdutyschedule billing-oncall← whoever is on that team todayunchecked
  6. palantiraction sync-renewal← reads Stripe and Salesforce bothunchecked

01The standard library

The ontology is a standard library of domain types.

@ontology/std defines the concepts every business shares: people, organizations, teams, money, and email. It's written in the Palantir-style ontology prelude as interfaces and value types. Platform preludes don't rebuild these. They say which of their own types implement them, so a project can accept “any organization” without caring which platform it came from.

@ontology/std/src/business.formastd
(export Email Currency Money
        Person Organization Team member-of)

(value-type Email String :pattern "^[^@]+@[^@]+$")
(value-type Currency (Enum :usd :eur :gbp :jpy))
(value-type Money {:amount Int :currency Currency})

(interface Person {:name String :email Email})
(interface Organization {:name String :domain (Option String)})
(interface Team {:name String})

(link-type member-of Person Team)
@ontology/salesforce/src/std.formasalesforce
(import "@ontology/std" :as std)

(implements std/Organization Account
  {:name Name :domain Website})
(implements std/Person Contact
  {:name Name :email Email})
ops/renewals.formapalantir
(object-type Renewal
  {:customer (Id std/Organization) ; Account or Customer
   :owner (Id std/Person)
   :amount std/Money})
Which platform types implement each standard library type
@ontology/std salesforce stripe okta github pagerduty
PersonContact, UserCustomer (individual)UserUserUser
OrganizationAccountCustomer (business)—Organization—
TeamPublic Group—GroupTeamTeam
member-ofGroup Member—group membershipteam membershipteam member
MoneyCurrency fieldamount + currency———
EmailEmail fieldemailloginemailemail

A dash means the platform has no such concept, and that's fine. Preludes implement what they actually have. The standard library is a package too: versioned, open to proposals, and swappable for an industry-specific one.

02–07The preludes

Every platform keeps its own words. They just share a type checker.

A prelude doesn't squeeze Salesforce into someone else's model. It teaches Forma Salesforce's own forms and elaborates them to the Metadata API's own files. Preludes share only Forma's types and the standard library, so a value in one project can refer to a declaration in another.

02The default prelude

@ontology/palantir

Objects, links, actions, and queries: the language the standard library is written in.

runs today · v0.1

The renewal

Your operational model took a year to build and lives inside one vendor's UI. When the contract comes up, the model is the one thing you can't take with you.

The ontology prelude is where Open Ontology started, and it's what @ontology/std is written in. Use it to model your own operations too: object types, link types that carry their own facts, actions that commit as one transaction, and named Datalog queries. It's the same core as Palantir Foundry's Ontology, as a plain file you own, and it elaborates to a portable IR that runs on Triplex.

  • → ontology IR
  • actions → Triplex transactions
  • queries → Datalog
oo add @ontology/palantir
field-service/model.formapalantir
(object-type WorkOrder {:title String :status String})
(object-type Technician {:name String})

(link-type assigned-to WorkOrder Technician
  {:assigned-at Instant})

(: assign (-> (Id WorkOrder) (Id Technician) (Action Bool)))
(define assign [order tech]
  (do! [_ (update! order {:status "assigned"})
        _ (link! assigned-to order tech {:assigned-at (now)})]
    true))

(query assigned-work
  :from WorkOrder
  :where (= status "assigned")
  :select [title])
runtime.query("assigned-work")Triplex
[{ "title": "Repair cooling pump" }]

03CRM metadata

@ontology/salesforce

Org metadata you can review, in Salesforce's own vocabulary.

preview

The sandbox refresh

Billing launched a new plan. Nobody added it to the Plan picklist, so the flow that depends on it fails in UAT. The only diff tool is a 40,000-line XML retrieve.

Objects, fields, picklists, validation rules, and record-triggered flows. They're checked at line:col and elaborated to the same -meta.xml files the Metadata API deploys. Here the picklist's values aren't typed in. They're the lookup keys from the billing project.

  • object → CustomObject
  • field → CustomField
  • record-flow → Flow
  • validation-rule → ValidationRule
oo add @ontology/salesforce
sales/opportunity.formasalesforce
(import "@acme/billing" :as billing)

(object Opportunity
  (field Plan__c (picklist-of billing/prices :by :lookup-key))
  (field Seats__c (Number :precision 6)))

(validation-rule seats-required Opportunity
  (and (= StageName "Closed Won") (blank? Seats__c))
  "Closed deals need a seat count.")

(record-flow mark-customer Opportunity
  :when (= StageName "Closed Won")
  (update! Account {:Type "Customer"}))
objects/Opportunity/fields/Plan__c.field-meta.xmlMetadata API
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
  <fullName>Plan__c</fullName>
  <type>Picklist</type>
  <valueSet><valueSetDefinition>
    <value><fullName>pro_monthly</fullName></value>  ← billing
    <value><fullName>pro_overage</fullName></value>  ← billing
  </valueSetDefinition></valueSet>
</CustomField>

04Billing config

@ontology/stripe

Your price book is a program. Write it like one.

preview

The pricing launch

New pricing ships from a spreadsheet. Someone clicks through the dashboard creating fourteen prices and mistypes one tier. The first overage invoice is off by 100×.

Products, prices, tiers, and meters elaborate to the exact Stripe API requests that would create them. Tier order, currencies, and meter references are checked first. The project exports prices as a typed value, so other projects can depend on the price book instead of copying it.

  • product → /v1/products
  • price → /v1/prices
  • meter → /v1/billing/meters
oo add @ontology/stripe
billing/pricing.formastripe
(export prices)

(product pro "Pro")

(price pro-monthly pro
  :lookup-key "pro_monthly" :currency :usd
  :recurring :month :unit-amount 2900)

(meter api-requests :event "api_request" :aggregate :sum)

(price pro-overage pro
  :lookup-key "pro_overage" :currency :usd
  :metered api-requests :tiers [[10000 0] [:inf 20]])

(define prices [pro-monthly pro-overage])
oo elaborate @acme/billingStripe API
POST /v1/products        { "id": "pro", "name": "Pro" }
POST /v1/billing/meters  { "event_name": "api_request",
                           "default_aggregation": { "formula": "sum" } }
POST /v1/prices          { "lookup_key": "pro_monthly",
                           "product": "pro", "currency": "usd",
                           "unit_amount": 2900,
                           "recurring": { "interval": "month" } }
POST /v1/prices          { "lookup_key": "pro_overage",
                           "billing_scheme": "tiered", … }

05Identity

@ontology/okta

Access that matches the org chart.

An app assignment names a Salesforce profile as a string. Someone renames the profile, and new hires get no CRM access for two weeks.

(import "@acme/sales" :as sales)

(group sales-eng
  :rule (= user.dept
           "Sales"))

(app-assignment salesforce
  sales-eng
  :profile sales/SalesUser)

→ Okta groups, rules, app assignments

sketch

06Source control

@ontology/github

Teams that come from identity.

CODEOWNERS names a team that was retired last quarter. Every pricing change now merges without a billing review.

(import "@acme/identity" :as id)

(team billing-eng
  :members (members id/billing))

(repo billing-service
  :codeowners
    {"/pricing/" billing-eng}
  :protect "main"
    {:reviews 2})

→ GitHub teams, CODEOWNERS, rulesets

sketch

07Incident response

@ontology/pagerduty

On-call follows the team.

The billing rotation still pages someone who moved to another team in March. They mute it. The invoice outage waits until morning.

(import "@acme/eng" :as eng)

(schedule billing-oncall
  :users (members
           eng/billing-eng)
  :rotation :weekly)

(escalation billing
  billing-oncall :after 15m)

→ PagerDuty schedules, escalation policies

sketch

··Your platform here

@you/anything

HubSpot. NetSuite. Workato. Your internal admin.

If a platform has config, it can have a prelude. Write its forms in Forma, say what each one elaborates to, and publish it for everyone.

$ oo init --prelude @you/anything
$ oo check
$ oo publish
Propose one on Discord ↗

08How a prelude works

A prelude is a Forma file. Elaboration is a typed function.

Since Forma 0.4, a form declares its syntax, its typed holes, its checks, and the IR it produces, all in one definition. Declarations become types that other projects can import. Follow one Stripe price from the prelude that defines it to the Salesforce project that depends on it.

01 · The prelude defines

The pattern sets the syntax and :types sets the holes. (Declares Price) makes each price a declaration that other files can refer to. The body is pure Forma that builds Stripe's request. Nothing in it can reach the network or the disk.

@ontology/stripe/src/prices.formaprelude
(export price Price PriceParams)

(form (price name product {:keys [lookup-key currency
                                  unit-amount recurring
                                  metered tiers]})
  "A Stripe price."
  :types {:name (Declares Price)
          :product (Refers Product)
          :lookup-key String
          :currency Currency
          :metered (Option (Refers Meter))
          :tiers (Option (List Tier))}
  :check stripe/check-tiers
  :ir PriceParams
  (stripe/price-params name product lookup-key …))
02 · You write

Stripe's words, in a file you can diff and review. An unknown option, a missing currency, or a tier out of order is an error at line:col.

billing/pricing.formasource
(price pro-monthly pro
  :lookup-key "pro_monthly" :currency :usd
  :recurring :month :unit-amount 2900)
03 · It elaborates to

Stripe's native request, with a source span on every value. Nothing gets squeezed into a shared model on the way. A reconciler such as Schematics can plan it against the live account.

oo elaborate @acme/billing --jsonPriceParams
{ "kind": "stripe/price", "name": "pro-monthly",
  "span": "billing/pricing.forma:3:1",
  "request": { "method": "POST", "path": "/v1/prices",
    "body": { "lookup_key": "pro_monthly", "product": "pro",
              "currency": "usd", "unit_amount": 2900,
              "recurring": { "interval": "month" } } } }
04 · Other projects see

The billing project's exports are a typed interface. To the sales project, billing/prices is a list of Price declarations, and picklist-of turns their lookup keys into the picklist's value type.

oo interface @acme/billingexports
(: prices (List stripe/Price))
;; pro-monthly  :lookup-key "pro_monthly"
;; pro-overage  :lookup-key "pro_overage"

;; in @acme/sales, Plan__c gets the type
;; (Union "pro_monthly" "pro_overage")

09Type-checked across projects

A rename in Salesforce is an error in Okta, before deploy.

Each project is a package with its own prelude, and projects import each other like modules. oo check elaborates the whole graph at once. A break in one platform's config is reported where another platform's config depends on it, before either one is deployed.

sales/profiles.formasalesforce · edited
-(profile SalesUser :license "Salesforce")
+(profile Sales_Rep :license "Salesforce")
oo checkexit 1
$ oo check
ok    @acme/billing   stripe
error sales/opportunity.forma:14:18
      "pro_annual" is not a Plan__c value. Plan__c values
      come from @acme/billing prices: pro_monthly, pro_overage
error identity/apps.forma:6:12
      @acme/sales has no profile SalesUser
      (renamed to Sales_Rep in sales/profiles.forma:3:10)
2 errors in 2 projects · nothing elaborated, nothing deployed
acme/                         ontology.json · oo.lock

@acme/billing   stripe
   ▲ prices
@acme/sales     salesforce
   ▲ profiles            ▲ Account
@acme/identity  okta      @acme/ops  palantir
   ▲ groups                 └─ also imports billing
@acme/eng       github
   ▲ teams
@acme/oncall    pagerduty

every ▲ is an import, and every import is typed
$ oo why @acme/sales.SalesUser
  used by identity/apps.forma:6:12

10The package manager

Preludes and projects are both packages.

A prelude package publishes a platform's language. A project package publishes your config for that platform, along with its typed exports. The standard library is a package as well. All of them get versions, a lockfile, and a registry entry, and anyone can post a prelude.

sales/ontology.jsonmanifest
{ "name": "@acme/sales",
  "version": "0.3.1",
  "prelude": "@ontology/salesforce@^0.1",
  "std": "@ontology/std@^0.1",
  "entry": "src/",
  "dependencies": {
    "@acme/billing": "^0.1"
  } }
oo add
Install a prelude or a project and pin it in oo.lock with a content hash.
oo check
Elaborate the whole project graph. Errors at file:line:col, plus --json.
oo elaborate
Print a project's native config: metadata files, API requests, or ontology IR.
oo plan
Diff the elaborated config against the live platform through a reconciler.
oo publish
Check, pack, and add a prelude or project to the registry index.

11Available today

The ontology checker ships today.

The default prelude's checker ships in v0.1 as pnpm model:check. It reports errors with line numbers, applies the runtime's naming and input rules, and lints for mistakes that fail silently, like a misspelled attribute or a value that's never replaced. The output below is real. In v0.1 the default prelude is spelled define-entity, define-relation, define-action, and define-datalog-query.

A draft with two mistakesexit 0
$ pnpm model:check examples/support-desk/draft.lisp
draft.lisp:43:1: warning: Action close-ticket sets :ticket/status without
  clearing it first; the previous value stays queryable. Add (clear ...
  :ticket/status) before the set to replace it
draft.lisp:68:1: warning: Query tickets-by-status matches :ticket/stauts,
  which no object or link declares; it will match nothing
ok examples/support-desk/draft.lisp (draft@0.1.0)
  …
2 warnings

Modeling for someone?

Everything an agent needs for the default prelude is one fetch away: the grammar, a seven-step procedure, the check command, runtime semantics, and limits. Every Forma block in it is checked in CI.

GET /llms-full.txt
Read the agent guide

12Status

What exists today, and what is designed.

Your config is the part that carries forward. Here's exactly which parts exist.

Runs today · v0.1

  • The Palantir-style ontology prelude, in Forma or TypeScript, compiled to one JSON IR
  • Located errors, naming rules, and lint with pnpm model:check
  • Actions as atomic, attributed, idempotent Triplex transactions
  • Named Datalog queries with joins, negation, aggregates, and ordering
  • Local in-memory runtime, with field-service and support-desk examples

Preview · v0.2 design

  • The salesforce, stripe, okta, github, and pagerduty preludes. Their syntax will change
  • @ontology/std, implements, cross-project imports, and type checking
  • The oo CLI, manifests, lockfile, and registry
  • oo plan against live platforms, persistence, and an npm release

Ground rules

  1. Preludes are pure Forma, with no install scripts
  2. Platform types implement the standard library; they don't replace it
  3. Each prelude elaborates to its platform's native config
  4. Every reference between projects is typed and checked before deploy
  5. Every elaborated value records its source span

Palantir, Foundry, Salesforce, Stripe, Okta, GitHub, and PagerDuty are trademarks of their owners. Preludes are community-built and modeled on public APIs. Open Ontology isn't affiliated with or endorsed by any of them.

13Start

Clone, check, run.

The package isn't on npm yet, so work inside a clone. Put a new model at examples/<domain>/model.lisp and copy examples/support-desk/run.ts next to it.

terminalNode 22+ · pnpm 10.11
git clone https://github.com/bjacobso/open-ontology.git
cd open-ontology
pnpm install --frozen-lockfile
pnpm model:check examples/support-desk/model.lisp
pnpm example:support-desk

14Community

Bring the platform you wish had a prelude.

Tell us which platform's config you're tired of clicking through, which domain type the standard library is missing, or which limit you hit. The first registry preludes will be the ones people ask for.

Join the Discord

15Part of the WorldVM family

Small experiments. Bigger worlds.

Open Ontology is one thread in a family of open experiments in software that can describe, remember, coordinate, and explain itself. WorldVM is where they meet: a runtime for operational worlds of entities, rules, goals, and actions.

Meet the whole family at worldvm.com ↗