Access to business data, and safe ways for agents to use it, receive a lot of attention. From my own experience, I had another question: do business AI agents also need shared definitions of the entities, states, and actions they work with, plus a way to maintain those definitions?
I built ontology-prototype to test whether I could turn that idea into a working system.
Adapted from my Japanese article.
Two experiences led me here.
In a previous role, I worked on deploying and customizing a platform with particular strengths in one kind of data. It worked well when those strengths matched a customer's needs. But actual business workflows crossed several systems and used many kinds of information. Working with only part of that data made it hard to support the workflow as a whole.
The other was a conversation with someone whose company owns several related businesses. They wanted to explore using forward-deployed engineers (FDEs), engineers who work directly with customers to improve operations, across those businesses. Two goals emerged: find changes that matter to each business and follow them through to an outcome, while making knowledge reusable across companies.
The companies would have different systems and data. That brought me back to the same question.
The operational loop
The prototype implements seven stages:
- Ingest snapshots from existing business sources.
- Map them into a shared business model stored in SQLite.
- Select candidates deterministically using declared conditions.
- Ask an AI model for explanations, possible causes, and proposed interventions.
- Have a human approve or reject each proposal.
- Execute approved proposals.
- Ingest later snapshots and evaluate the observed outcomes against a declared target and guardrails.
The facts and stage records are materialized in SQLite under the workspace's .ontology/ directory. The system keeps an internal representation of what it has ingested.
Execution and outcome observation have separate destinations and sources. The current execution adapter writes reminders to a local CSV. Later business outcomes come from new exports of the external business systems, not from that reminder file.
Repository structure
The implementation is a monorepo. Every application calls the same shared core.
| Path | Responsibility |
|---|---|
packages/core/ |
Shared Python package for definition validation and the operational loop |
apps/cli/ |
CLI commands for each stage |
apps/api/ |
Local FastAPI app for editing and validating definitions and triggering stages |
apps/web/ |
React UI that accesses the core through the API |
examples/ |
Workspace definitions and synthetic data |
The example: completed work waiting to be invoiced
For the demo, I used a workflow where completed jobs sit uninvoiced for too long.
Jobs are recorded in a job-management system; invoices are recorded in a separate invoicing system. Each job's assignee issues the invoice manually. Nothing tells them when a completed job is ready for billing, so completed work can remain uninvoiced.
The shared model defines three entities: Job, Invoice, and Person. It also defines two events: JobCompleted and InvoiceIssued. An event records something that happened to an entity, with a timestamp.
The scenario sets a target: reduce the median time from job completion to invoice issuance from a baseline of nine days to less than four days, without increasing the invoice-error rate. The comparison is strict: exactly four days misses the target.
Candidates are jobs that have been completed for more than two days without an invoice. The intervention is a reminder to the assignee.
All data is synthetic. The current adapter records the reminder in a local CSV; it does not send email or write to an external business system.
Four files define the workflow
A workspace is a directory containing four YAML files. The core reads these definitions as data. Loading them does not execute Python or SQL, or expand environment variables.
| File | Defines | References |
|---|---|---|
domain.yaml |
Entities, properties, references, events, and candidate-selection conditions | — |
mapping.yaml |
Source fields mapped to entities and events | domain.yaml |
evaluation.yaml |
Outcome metric, target, guardrails, and their metric definitions | domain.yaml |
agent.yaml |
Diagnosis task, expected deliverables, and permitted action schemas | domain.yaml |
The domain defines what exists in the workflow, what can happen to it, and how to select entities from those events. Properties are typed and can reference another entity with ref.
An excerpt from examples/job-invoicing/domain.yaml:
entities:
Job:
properties:
status:
type: string
required: true
assignee:
type: ref
ref: Person
# Invoice and Person are also declared.
events:
JobCompleted:
subject: Job
InvoiceIssued:
subject: Job
properties:
invoice:
type: ref
ref: Invoice
projections:
CompletedButNotInvoiced:
subject: Job
select:
required_events: [JobCompleted]
absent_events: [InvoiceIssued]
elapsed: { comparator: gt, value: 2, unit: days }
A job references its assignee. An invoice references its job. Invoice issuance is recorded as an event whose subject is the job. Although the source records live in different systems, their relationships are expressed in one shared model.
The other files refer to names from this domain.
For example, mapping.yaml declares that a job-management CSV row with status: completed and a completion timestamp asserts a JobCompleted event:
- event: JobCompleted
subject:
entity: Job
id:
field: id
when:
op: and
args:
- op: eq
field: status
value: completed
- op: not_null
field: completed_at
occurred_at:
field: completed_at
as: datetime
The evaluation and agent definitions follow the same pattern. I will return to them in the evaluation and AI sections.
Following one job, C01, shows what these definitions connect:
| Stage | How C01 is represented |
|---|---|
| Ingestion | A source row becomes Job/C01 and a completion event |
| Candidate selection | Its completion event and missing invoice make it a diagnosis candidate |
| Diagnosis and proposal | A SendReminder proposal references Job/C01 through related_entity
|
| Later ingestion and evaluation | Invoice issuance has the same Job/C01 subject, allowing completion and issuance to be paired |
Reads, reasoning, actions, and evaluation all refer to the same entity. That makes checking those references part of the foundation.
The shared core validates the four files together. ontology check checks types, required fields, and cross-file references. A mapping to an undefined entity or an agent referring to a nonexistent projection is an error.
This is what I mean by managing shared definitions in this prototype: making their consistency mechanically checkable.
Palantir Ontology was a reference point for this design, particularly the idea of connecting real-world entities and relationships with actions. I applied that idea within the scope of a small operational loop.
Keep ingestion evidence separate from business facts
The demo ingests CSV snapshots exported from each system. Consider this row from jobs-current.csv:
id,status,assignee,completed_at,invoiced_at
C01,completed,taro,2026-10-17,
Through the mapping, it produces three things: job C01, person taro, and a fact asserting that C01 was completed on October 17, 2026.
The completion timestamp becomes occurred_at: when the event happened in the business. In this example, observed_at records when the snapshot was ingested. Keeping both helps relate a run's inputs to when the system learned about them.
The system retains two kinds of records:
- Ingestion evidence: the snapshot bytes and per-row observation records.
- Canonical business facts: entities and events in SQLite.
Events are append-only. An event's identity is determined by its type, subject, and occurrence time.
An invoice-issuance fact can arrive again from the invoicing export. The event is not duplicated; another observation is linked to the existing fact. If two records disagree on the occurrence time, both facts are retained. The system cannot tell whether the difference is a correction or a separate occurrence, so that decision remains with the operator.
I treat the external systems and business documents as the sources of truth. The internal model and AI output alone do not establish that a business event happened. Keeping conflicting facts and repeated observations preserves the records behind those assertions.
Those sources need not be clean or even digital. The information needed to establish a fact may still be on paper or in someone's memory.
Limit what the AI sees and what it decides
Candidate selection is deterministic.
The projection from domain.yaml is evaluated against events at an explicit --as-of time. With October 21, 2026 as the evaluation date, C01 is four days past completion and C02 is three days past completion. Both qualify. C03, at exactly two days, does not.
The candidate set and input references are frozen for each run. This preserves what that execution examined and selected; it does not reconstruct the entire system at an arbitrary historical instant.
The model receives those candidates, their evidence, the task, and the schemas of permitted actions.
In agent.yaml, this demo permits only SendReminder, with typed, required parameters for the recipient, related job, and reason:
projection: CompletedButNotInvoiced
task:
goal: Identify likely causes of delayed invoicing and suggest an intervention.
deliverables: [explanation, hypotheses, follow_up_questions, action_proposals]
permitted_actions:
SendReminder:
description: Send a billing-readiness reminder to the assignee.
params:
target: { type: ref, ref: Person, required: true }
related_entity: { type: ref, ref: Job, required: true }
reason: { type: string, required: true }
The output contract is a single JSON object. Its shape is:
{
"explanation": "...",
"hypotheses": [
{"statement": "...", "evidence": ["fact-...", "obs-..."]}
],
"follow_up_questions": ["..."],
"action_proposals": [
{
"action": "SendReminder",
"params": {
"target": "taro",
"related_entity": "C01",
"reason": "..."
}
}
]
}
The core validates that output before saving executable proposals. Disallowed actions, invalid parameters, and nonexistent references are rejected. Evidence IDs cited by hypotheses are also checked; unresolved references are marked unverified.
I wanted the model to produce proposals I could inspect and validate. Mechanical validation can establish consistency with definitions and references. It cannot establish that a cause hypothesis is correct or that an intervention makes sense for the business.
Human review is the conservative choice in this version. I would like to automate decisions that eventually become stable enough not to need it.
In the recorded model comparison, the most useful differences appeared in explanations and questions. Three models each ran once on the same frozen input. All passed the output contract, but the recorded assessment found differences in how specifically they cited evidence and how useful their follow-up questions were. This was one trial per model, not evidence of a general model ranking.
The model rated most highly noticed that both delayed jobs belonged to the same assignee and asked whether the invoicing procedure was documented and whether anything else was blocking it. Another used less specific evidence citations and generic reminder text.
There is also an obvious limit to this demo: the only intervention is a reminder. A rule could construct that proposal without an AI model.
My conclusion from the prototype was that AI was more useful for organizing possible explanations and identifying what a human should investigate when the cause was uncertain.
Separate proposals, approvals, and execution
A validated proposal is saved with its action and parameters. Human approval or rejection is recorded separately. Proposals and decisions are append-only; the latest decision determines the current approval state.
Approval is bound to the proposal's contents. A digest of the action and parameters is stored with the decision and checked immediately before execution. If the payload has changed, that approval does not authorize it. Execution is also refused if the current definition no longer permits the action.
Approval alone executes nothing.
The digest check answers one question: is this the payload that was approved? Whether it is still appropriate is a separate question. If an invoice was issued through another path between approval and execution, the reminder might no longer be needed. That is outside this mechanism's validation scope.
An adapter separates the destination from the core. The current adapter appends one SendReminder row to a local CSV. It does not send email or write to an external SaaS system.
Before calling the adapter, the executor records an intent: an attempt is about to happen. After the call, it records the outcome and a receipt when delivery succeeds.
The retry policy distinguishes success, confirmed failure, and an uncertain outcome:
| Recorded state | What happens on another execution request |
|---|---|
| Succeeded | Return the stored receipt without calling the adapter again |
| Failed with confirmation that no effect occurred | Allow a retry |
| Uncertain | Retry only if the adapter guarantees deduplication for the same idempotency key |
| Unapproved, rejected, or changed payload | Refuse execution |
This does not guarantee exactly-once delivery. It makes the next permitted step depend on the recorded outcome and the adapter's guarantees.
Evaluate the business outcome separately from execution
Writing a reminder does not mean an invoice was issued.
Execution never creates an InvoiceIssued event. The business outcome has to arrive through a later source snapshot. In the demo, later CSVs assert invoice issuance for C01 through C03.
The evaluation definition declares what counts as improvement:
interval:
start_event: JobCompleted
end_event: InvoiceIssued
metric: duration
aggregation: median
unit: days
baseline:
value: 9
target:
comparator: lt
value: 4
metrics:
invoice_error_rate:
entity: Invoice
ref: job
flag: error
guardrails:
- name: invoice-error-rate
metric: invoice_error_rate
comparator: not_increase
Evaluation pairs start and end events by subject, then splits subjects into baseline and after cohorts using an explicit --boundary. Jobs still waiting for an invoice remain in the report as pending, along with their elapsed time.
The target and guardrails are judged separately. Missing evidence can leave the overall result unverified.
The walkthrough produces:
baseline: median 9 days (3 paired)
after: median 3 days (3 paired, 1 pending)
change: 6 days (improvement)
target lt 4 days: attained
guardrail invoice-error-rate: passed
overall: success
That success label has a specific limit. The median includes only completed intervals. A job still waiting for an invoice does not enter that aggregate. The evaluation can therefore report success while unresolved delays remain.
I kept pending jobs and their elapsed times visible so the aggregate could be read alongside the completion counts. A possible next step would be an additional condition on the age of pending jobs. That would be a separate requirement from the median target.
Overall success means that the after cohort met the target and all guardrails passed. A missed requirement is not_met; insufficient evidence can produce unverified.
In a recorded run with a live model, the synthetic cohorts went from a nine-day median to three days, but invoice-error evidence was missing. The overall result was unverified. Reporting insufficient evidence despite a favorable metric is a property I want to keep.
The evaluation boundary is supplied by the operator. It is not derived from the proposal or execution records. Later facts can be linked to proposals for traceability, but the report measures observed differences between cohorts; it does not establish causation.
These are demo results, not evidence of a real business improvement. What the prototype demonstrates is that the stages connect, and that ontology trace can follow the same subject through their records.
Try the same structure on another workflow
One invoicing example cannot establish whether this is shared infrastructure or an invoicing-specific implementation.
I added a second workflow: first-response SLA monitoring for support tickets. It has a similar structure: a start event occurs, an expected end event is still missing after a threshold, and a reminder goes to the responsible person.
Getting the second workflow to run required moving candidate-selection logic, guardrail metric definitions, and multiple-source support from workflow-specific code into declarations. With those changes, selection through evaluation ran using the same four-file structure.
The conclusion is limited: workflows with at least this common structure can use the same definition model. It does not establish generality across arbitrary business processes.
What I learned, and what remains
I was able to implement a shared model that connects ingestion, proposals, approval, execution, and evaluation across records from different systems.
Before building it, I expected the hardest technical boundaries to be ingesting external data and returning actions to external destinations. Those became the parts I spent the most attention on.
At the input boundary, I separated observation evidence from business facts, retained conflicting facts, and linked repeated observations without duplicating events. At the output boundary, I separated proposals, approvals, and execution, and made retry behavior depend on recorded outcomes.
Those two boundaries worked as the entry and exit points of the shared definitions.
Production use would need much more: API and database connections, authentication, continuing incremental ingestion, real delivery adapters, write permissions, and recovery from distributed failures. Reviewer authentication and authorization would also need a different design.
I am particularly interested in how far a common interface could extend to data that does not fit naturally into relational tables, such as images, video, or graph data, without forcing application code to handle every storage detail.
But the hardest operational work may be finding and obtaining the information in the first place. There will not always be a well-maintained API waiting to be connected. Someone has to find the person who holds the relevant records and arrange for them to be supplied continuously.
Someone also has to understand the business well enough to write the shared definitions, then maintain them as the business changes. Who creates and maintains the model remains a substantial problem beyond the software.
The uncertainty this prototype reduced was technical: could I connect a workflow spanning multiple systems through shared definitions and keep the resulting process traceable?
The next test is one real workflow: can I obtain its data continuously, and can the people responsible for it judge the proposals? I would like to find an opportunity to put that into practice.
Top comments (2)
The 9 day to 3 day median looks strong, but it excludes the 1 pending job, so the slowest cases drop out of the headline number. With only 3 paired jobs in each cohort, one late invoice would move that median a lot, and since the only intervention is a reminder, the result says more about nudging than about fixing why invoicing stalls.
Thanks for reading! These are synthetic data—the goal was to test the end-to-end workflow. Actual business impact still needs validation.