AI & Machine Learning

REA Framework & Bank Ontology: A Complete Tutorial

A hands-on tutorial on the REA (Resources, Events, Agents) framework applied to banking ontology — from McCarthy's 1982 origins to building a working OWL ontology with Python, RDFLib, SPARQL queries, and AI/ML integration patterns.

Share this article
Comments
Share:
A hands-on tutorial on the REA (Resources, Events, Agents) framework applied to banking ontology — from McCarthy's 1982 origins to building a working OWL ontology with Python, RDFLib, SPARQL queries, and AI/ML integration patterns.
Table of Contents

REA Framework & Bank Ontology: A Complete Tutorial

Level: Intermediate
Time to complete: 60–90 minutes
Prerequisites: Basic understanding of databases, some familiarity with Python or JSON; no prior ontology experience required


Learning Objectives

By the end of this tutorial you will be able to:

  • Explain the three REA primitives and how they relate to each other
  • Map real banking transactions onto the REA model
  • Read and write basic REA Turtle/OWL notation
  • Build a working REA ontology for a simple loan lifecycle using Python and RDFLib
  • Understand how REA connects to FIBO, XBRL, and modern AI/ML data pipelines in banking

Table of Contents

  1. What is REA? The 1982 Idea That Never Got Old
  2. The Three Primitives — Resources, Events, Agents
  3. REA Relationships — How the Pieces Connect
  4. What Makes It an Ontology?
  5. Why Banks Use REA Ontology
  6. Tutorial: Build a Bank REA Ontology in Turtle
  7. Tutorial: Build the Same Ontology in Python with RDFLib
  8. Real-World Example: Modelling a Loan Lifecycle
  9. REA Meets FIBO — The Industry Standard Layer
  10. REA as a Foundation for AI/ML in Banking
  11. Exercises
  12. Further Reading

Part 1 — What is REA? The 1982 Idea That Never Got Old

In 1982, William E. McCarthy, a professor at Michigan State University, published a paper titled “The REA Accounting Model: A Generalized Framework for Accounting Systems in a Shared Data Environment.” The core argument was simple and radical at the same time: traditional double-entry bookkeeping destroys information.

When a bank records a debit of $10,000 to a loan receivable and a credit of $10,000 to cash, it captures the accounting result of a disbursement — but it throws away the economic reality underneath it: who requested the loan, what collateral was pledged, which officer approved it, what the disbursement event actually consisted of. You get the balance sheet entry; you lose the story.

McCarthy’s REA model proposed recording the economic reality directly, in three kinds of things:

┌───────────────────────────────────────────┐
│              REA FRAMEWORK                │
│                                           │
│   RESOURCE ←── EVENT ──→ RESOURCE         │
│                  │                        │
│               AGENT(s)                    │
└───────────────────────────────────────────┘
  • Resource — something of economic value that is increased or decreased
  • Event — an economic occurrence that changes the value of resources
  • Agent — an individual or organization that participates in an event

That’s the whole model. Three nouns, and the relationships between them. Everything in financial accounting — every ledger, every statement, every report — can be derived from instances of these three things.

Why does this matter today, 40+ years later? Because every modern banking challenge — regulatory reporting, AI-driven decisioning, data interoperability between core systems — requires exactly what REA provides: a single, unambiguous semantic layer that describes what actually happened economically, not just what the accounting system recorded.


Part 2 — The Three Primitives: Resources, Events, Agents

2.1 Resources

A Resource is anything of economic value that a bank holds, lends, borrows, or exchanges. In the REA model, a resource has a quantity or value that can be increased or decreased by events.

Banking resources — concrete examples:

ResourceDescriptionKey Attribute
CashPhysical or central bank depositsBalance, currency
LoanReceivableAmount owed by a borrowerPrincipal, rate, term
DepositCustomer’s claim on the bankBalance, rate type
SecurityPositionHoldings of bonds, equitiesUnits, market value
CreditLineApproved but undrawn creditLimit, drawn amount
CollateralAsset pledged against a loanType, appraised value
FeeReceivableEarned but unpaid feesAmount, due date

A critical point: a Resource in REA is not an account. A LoanReceivable account in the general ledger is a derived view of loan resource instances. The resource itself is the actual loan — its terms, its parties, its economic substance.

2.2 Events

An Event is an economic occurrence — something that happened and changed the quantity or value of one or more resources. Events are the heart of the REA model. They are what gets recorded; everything else is computed from them.

Banking events — concrete examples:

EventDecrementsIncrementsDescription
LoanDisbursementCashLoanReceivableBank pays out loan principal
LoanRepaymentLoanReceivableCashBorrower repays principal
InterestPaymentCash (borrower)InterestIncomeBorrower pays interest
DepositReceivedCashDepositLiabilityCustomer deposits funds
WithdrawalDepositLiabilityCashCustomer withdraws funds
SecurityPurchaseCashSecurityPositionBank buys a bond
SecuritySaleSecurityPositionCashBank sells a bond
FeeChargeFeeReceivableFeeIncomeBank charges a service fee
WireTransferDepositLiability (sender)DepositLiability (receiver)Funds move between accounts

Notice that every event has a duality: something is given up and something is received. This is the REA equivalent of double-entry — but instead of debits and credits, you have economic give and receive.

2.3 Agents

An Agent is a person or organization that participates in an event — either as the provider (giving up the resource) or the receiver (gaining it).

Banking agents — concrete examples:

AgentRoleExamples
CustomerBorrower, depositor, investorRetail client, SME, corporate
BankLender, custodian, counterpartyThe institution itself
EmployeeInternal actor, authorizerLoan officer, trader, teller
CounterpartyTrading party, correspondentAnother bank, broker-dealer
RegulatorSupervisory authorityOCC, Fed, FCA, ECB
ThirdPartyService provider, guarantorInsurer, credit bureau, servicer

In REA, agents are linked to events through participation relationships. A loan disbursement event has the bank participating as provider (of cash) and the customer participating as receiver. The same customer then participates as provider (of repayments) in subsequent loan repayment events.


Part 3 — REA Relationships: How the Pieces Connect

The primitives alone aren’t enough. REA defines four core relationship types that wire everything together.

3.1 Duality

The most important relationship in REA. Every economic exchange involves at least two events: one that decrements a resource and one that increments a resource. These paired events are connected by a duality relationship.

   LoanDisbursement ←──────── duality ────────→ LoanRepayment
   (Bank gives cash)                            (Bank receives cash back)

   DepositReceived ←──────── duality ────────→ Withdrawal
   (Bank receives cash)                        (Bank gives cash back)

The duality relationship is what makes REA accountable. Every give must have a corresponding receive, either now (spot transaction) or in the future (credit transaction via commitments — see 3.4).

3.2 Participation

Participation links an Agent to an Event, specifying their role.

   Customer ──── participates-in ──── LoanRepayment ──── participates-in ──── Bank
   (as provider)                                                              (as receiver)

Participation can carry additional attributes: the timestamp when the agent authorized the event, their role designation, the delegation chain, and whether they are the primary or secondary participant. For compliance purposes, participation is often the most auditable part of the model — it captures who did what.

3.3 Stockflow

Stockflow links an Event to the Resources it affects. It distinguishes whether the event increments or decrements the resource quantity.

   LoanDisbursement ──── decrements ──── CashResource
   LoanDisbursement ──── increments ──── LoanReceivableResource

The name “stockflow” comes from the distinction between stocks (resources, which have a balance at a point in time) and flows (events, which change that balance over time). Think of it exactly like physics: water level in a tank (stock) is determined by the flow of water in and out.

3.4 Commitment (REA Extension)

McCarthy later extended the model with Commitments — planned or promised economic events that haven’t happened yet. In banking, this is enormously useful.

   COMMITMENT ──── fulfilled-by ──── EVENT

        ├── reserves Resource (notional)
        └── involves Agent (as obligated party)

Banking uses of commitments:

  • Loan approval commits the bank to future disbursement
  • Credit line agreement commits the bank to lend up to a limit
  • Repayment schedule commits the borrower to future payments
  • Trading order commits a counterparty to a future securities exchange

Commitments let you model the lifecycle of a banking product — from application through to final settlement — without conflating promises with economic facts.


Part 4 — What Makes It an Ontology?

The word “ontology” comes from philosophy (the study of what exists), but in computer science it means something specific: a formal, machine-readable vocabulary of concepts and their relationships in a domain.

An REA framework becomes an REA ontology when you express it in a formal language — typically OWL (Web Ontology Language) using RDF (Resource Description Framework) syntax — that allows:

  1. Inference: a reasoner can derive new facts from existing ones
  2. Interoperability: any system that understands OWL can consume the data
  3. Constraint checking: the ontology can validate that data conforms to the model
  4. Querying: SPARQL can retrieve facts across the entire graph

The basic building blocks in OWL/RDF:

OWL TermREA MeaningExample
owl:ClassA type of REA primitiverea:Resource, rea:Event
owl:ObjectPropertyA relationshiprea:duality, rea:participatesIn
owl:DatatypePropertyA scalar attributerea:quantity, rea:date
owl:IndividualA specific instanceloan:Disbursement_2026_001
rdfs:subClassOfSpecializationbank:LoanReceivable subClassOf rea:Resource

When you hear “banking ontology” — whether it’s the FIBO (Financial Industry Business Ontology), XBRL taxonomies, or a bank’s internal knowledge graph — REA often sits underneath as the economic substrate, even if not explicitly named.


Part 5 — Why Banks Use REA Ontology

5.1 Regulatory Reporting

Regulatory reporting (Basel III, FINREP, COREP, FR Y-9C) requires banks to slice the same economic reality many different ways: by risk weight, by maturity bucket, by counterparty type, by product. If the underlying data is stored as accounting entries, each new regulatory view requires a custom ETL pipeline. If the underlying data is stored as REA events, the regulatory view is just a different query over the same graph.

5.2 Interoperability Between Core Systems

A large bank typically has dozens of core systems: loan origination, core banking, trade finance, treasury, collateral management, risk engines. Each has its own data model. REA ontology provides a canonical semantic layer that all systems can map to — so a loan in the origination system and a loan receivable in the core banking system are understood as representations of the same rea:Resource instance.

5.3 Audit Trails and Compliance

REA naturally produces audit trails. Every economic event is a first-class record, with participating agents, timestamps, and links to the resources affected. This maps directly to what regulators require: who authorized what, when, and what changed as a result.

5.4 AI and ML Data Pipelines

This is the most exciting modern application. LLMs and ML models trained on or querying banking data need to understand what entities mean, not just what columns exist. An REA ontology tells an AI agent that a LoanRepayment event is semantically related to a LoanDisbursement event through a duality relationship — enabling a credit risk agent to reason about the full lifecycle of a loan rather than treating each transaction row as an isolated fact.

5.5 FIBO Alignment

The Financial Industry Business Ontology (FIBO), maintained by the Object Management Group (OMG), is the closest thing banking has to an industry-standard ontology. FIBO’s transaction and event layers are heavily influenced by REA. If your bank is building FIBO-compliant data models, understanding REA is the prerequisite.


Part 6 — Tutorial: Build a Bank REA Ontology in Turtle

Turtle (.ttl) is the most readable syntax for RDF/OWL. Let’s build a minimal but complete REA ontology for a bank loan product, step by step.

Step 1: Set Up Prefixes

Every Turtle file starts with namespace declarations. Think of these as imports.

@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# Our REA base ontology namespace
@prefix rea:  <http://superml.dev/ontology/rea#> .

# Our bank-specific extension namespace
@prefix bank: <http://superml.dev/ontology/bank#> .

# Our instance data namespace (actual loans, events, etc.)
@prefix inst: <http://superml.dev/data/bank#> .

<http://superml.dev/ontology/bank>
    a owl:Ontology ;
    rdfs:label "SuperML Bank REA Ontology" ;
    rdfs:comment "REA-based ontology for core banking operations" .

Step 2: Define the Core REA Classes

# ─── REA PRIMITIVES ────────────────────────────────────────────────────────────

rea:Resource a owl:Class ;
    rdfs:label "Economic Resource" ;
    rdfs:comment "Something of economic value that can be increased or decreased." .

rea:Event a owl:Class ;
    rdfs:label "Economic Event" ;
    rdfs:comment "An occurrence that changes the quantity or value of resources." .

rea:Agent a owl:Class ;
    rdfs:label "Economic Agent" ;
    rdfs:comment "A person or organization that participates in economic events." .

rea:Commitment a owl:Class ;
    rdfs:label "Economic Commitment" ;
    rdfs:comment "A promise or obligation to perform a future economic event." .

Step 3: Define REA Relationships (Object Properties)

# ─── REA RELATIONSHIPS ─────────────────────────────────────────────────────────

rea:duality a owl:ObjectProperty ;
    rdfs:label "duality" ;
    rdfs:comment "Links a give-event to its corresponding receive-event." ;
    rdfs:domain rea:Event ;
    rdfs:range  rea:Event .

rea:participatesIn a owl:ObjectProperty ;
    rdfs:label "participates in" ;
    rdfs:domain rea:Agent ;
    rdfs:range  rea:Event .

rea:asProvider a owl:ObjectProperty ;
    rdfs:subPropertyOf rea:participatesIn ;
    rdfs:label "participates as provider" ;
    rdfs:comment "The agent giving up the resource in this event." .

rea:asReceiver a owl:ObjectProperty ;
    rdfs:subPropertyOf rea:participatesIn ;
    rdfs:label "participates as receiver" ;
    rdfs:comment "The agent gaining the resource in this event." .

rea:decrements a owl:ObjectProperty ;
    rdfs:label "decrements" ;
    rdfs:comment "Stockflow: this event reduces the quantity of this resource." ;
    rdfs:domain rea:Event ;
    rdfs:range  rea:Resource .

rea:increments a owl:ObjectProperty ;
    rdfs:label "increments" ;
    rdfs:comment "Stockflow: this event increases the quantity of this resource." ;
    rdfs:domain rea:Event ;
    rdfs:range  rea:Resource .

rea:fulfilledBy a owl:ObjectProperty ;
    rdfs:label "fulfilled by" ;
    rdfs:comment "The actual event that fulfils this commitment." ;
    rdfs:domain rea:Commitment ;
    rdfs:range  rea:Event .

rea:reservesResource a owl:ObjectProperty ;
    rdfs:label "reserves resource" ;
    rdfs:domain rea:Commitment ;
    rdfs:range  rea:Resource .

Step 4: Define Banking-Specific Subclasses (Resources)

# ─── BANKING RESOURCES ─────────────────────────────────────────────────────────

bank:CashResource a owl:Class ;
    rdfs:subClassOf rea:Resource ;
    rdfs:label "Cash" ;
    rdfs:comment "Central bank deposits or vault cash held by the bank." .

bank:LoanReceivable a owl:Class ;
    rdfs:subClassOf rea:Resource ;
    rdfs:label "Loan Receivable" ;
    rdfs:comment "Principal amount owed to the bank by a borrower." .

bank:DepositLiability a owl:Class ;
    rdfs:subClassOf rea:Resource ;
    rdfs:label "Deposit Liability" ;
    rdfs:comment "The bank's obligation to return funds deposited by a customer." .

bank:InterestIncome a owl:Class ;
    rdfs:subClassOf rea:Resource ;
    rdfs:label "Interest Income" ;
    rdfs:comment "Earned interest revenue." .

bank:Collateral a owl:Class ;
    rdfs:subClassOf rea:Resource ;
    rdfs:label "Collateral" ;
    rdfs:comment "Asset pledged by a borrower to secure a loan." .

Step 5: Define Banking-Specific Subclasses (Events)

# ─── BANKING EVENTS ────────────────────────────────────────────────────────────

bank:LoanDisbursement a owl:Class ;
    rdfs:subClassOf rea:Event ;
    rdfs:label "Loan Disbursement" ;
    rdfs:comment "The bank pays out loan principal to the borrower." .

bank:LoanRepayment a owl:Class ;
    rdfs:subClassOf rea:Event ;
    rdfs:label "Loan Repayment" ;
    rdfs:comment "The borrower repays principal to the bank." .

bank:InterestPayment a owl:Class ;
    rdfs:subClassOf rea:Event ;
    rdfs:label "Interest Payment" ;
    rdfs:comment "The borrower pays interest to the bank." .

bank:DepositReceived a owl:Class ;
    rdfs:subClassOf rea:Event ;
    rdfs:label "Deposit Received" ;
    rdfs:comment "The bank receives funds from a customer." .

bank:Withdrawal a owl:Class ;
    rdfs:subClassOf rea:Event ;
    rdfs:label "Withdrawal" ;
    rdfs:comment "The bank pays out funds to a customer." .

Step 6: Define Banking Agents

# ─── BANKING AGENTS ────────────────────────────────────────────────────────────

bank:RetailCustomer a owl:Class ;
    rdfs:subClassOf rea:Agent ;
    rdfs:label "Retail Customer" .

bank:CorporateCustomer a owl:Class ;
    rdfs:subClassOf rea:Agent ;
    rdfs:label "Corporate Customer" .

bank:BankEntity a owl:Class ;
    rdfs:subClassOf rea:Agent ;
    rdfs:label "Bank Entity" ;
    rdfs:comment "The bank itself, acting as a party to transactions." .

bank:LoanOfficer a owl:Class ;
    rdfs:subClassOf rea:Agent ;
    rdfs:label "Loan Officer" ;
    rdfs:comment "Employee who authorizes loan decisions." .

Step 7: Add Scalar Properties (Datatype Properties)

# ─── DATA PROPERTIES ───────────────────────────────────────────────────────────

rea:quantity a owl:DatatypeProperty ;
    rdfs:label "quantity" ;
    rdfs:domain rea:Resource ;
    rdfs:range  xsd:decimal .

rea:eventDate a owl:DatatypeProperty ;
    rdfs:label "event date" ;
    rdfs:domain rea:Event ;
    rdfs:range  xsd:date .

rea:amount a owl:DatatypeProperty ;
    rdfs:label "amount" ;
    rdfs:domain rea:Event ;
    rdfs:range  xsd:decimal .

rea:currency a owl:DatatypeProperty ;
    rdfs:range  xsd:string .

bank:interestRate a owl:DatatypeProperty ;
    rdfs:label "interest rate" ;
    rdfs:domain bank:LoanReceivable ;
    rdfs:range  xsd:decimal .

bank:maturityDate a owl:DatatypeProperty ;
    rdfs:label "maturity date" ;
    rdfs:domain bank:LoanReceivable ;
    rdfs:range  xsd:date .

bank:loanId a owl:DatatypeProperty ;
    rdfs:label "loan ID" ;
    rdfs:range  xsd:string .

Step 8: Create Instance Data

Now let’s record an actual loan disbursement in our ontology:

# ─── INSTANCE DATA: LOAN DISBURSEMENT ─────────────────────────────────────────

# The Agents
inst:customer_john_doe a bank:RetailCustomer ;
    rdfs:label "John Doe" .

inst:first_national_bank a bank:BankEntity ;
    rdfs:label "First National Bank" .

inst:officer_jane_smith a bank:LoanOfficer ;
    rdfs:label "Jane Smith, Loan Officer" .

# The Resources
inst:bank_cash_usd a bank:CashResource ;
    rdfs:label "Bank USD Cash Pool" ;
    rea:quantity  "50000000.00"^^xsd:decimal ;
    rea:currency  "USD" .

inst:loan_receivable_001 a bank:LoanReceivable ;
    rdfs:label "Personal Loan #L-2026-001" ;
    bank:loanId       "L-2026-001" ;
    rea:quantity      "25000.00"^^xsd:decimal ;
    rea:currency      "USD" ;
    bank:interestRate "0.0875"^^xsd:decimal ;
    bank:maturityDate "2031-06-11"^^xsd:date .

# The Disbursement Event
inst:disbursement_001 a bank:LoanDisbursement ;
    rdfs:label      "Disbursement for Loan L-2026-001" ;
    rea:eventDate   "2026-06-11"^^xsd:date ;
    rea:amount      "25000.00"^^xsd:decimal ;
    rea:currency    "USD" ;
    # Stockflow: what this event does to resources
    rea:decrements  inst:bank_cash_usd ;
    rea:increments  inst:loan_receivable_001 ;
    # Participation: who is involved
    rea:asProvider  inst:first_national_bank ;
    rea:asReceiver  inst:customer_john_doe .

# The first scheduled Repayment Event
inst:repayment_001a a bank:LoanRepayment ;
    rdfs:label      "Repayment 1 for Loan L-2026-001" ;
    rea:eventDate   "2026-07-11"^^xsd:date ;
    rea:amount      "516.31"^^xsd:decimal ;
    rea:currency    "USD" ;
    rea:decrements  inst:loan_receivable_001 ;
    rea:increments  inst:bank_cash_usd ;
    rea:asProvider  inst:customer_john_doe ;
    rea:asReceiver  inst:first_national_bank .

# Duality: the disbursement and repayment are dual events
inst:disbursement_001 rea:duality inst:repayment_001a .

Part 7 — Tutorial: Build the Same Ontology in Python with RDFLib

RDFLib is the standard Python library for working with RDF/OWL graphs. Let’s build the same ontology programmatically.

Setup

pip install rdflib

7.1 Core Setup and Namespaces

from rdflib import Graph, Namespace, Literal, URIRef
from rdflib.namespace import RDF, RDFS, OWL, XSD
from datetime import date

# Initialize graph
g = Graph()

# Define namespaces
REA  = Namespace("http://superml.dev/ontology/rea#")
BANK = Namespace("http://superml.dev/ontology/bank#")
INST = Namespace("http://superml.dev/data/bank#")

g.bind("rea",  REA)
g.bind("bank", BANK)
g.bind("inst", INST)
g.bind("owl",  OWL)
g.bind("xsd",  XSD)

7.2 Define Classes Programmatically

def define_class(uri, label, comment=None, parent=OWL.Thing):
    """Helper: declare an OWL class with label and optional comment."""
    g.add((uri, RDF.type,        OWL.Class))
    g.add((uri, RDFS.label,      Literal(label)))
    g.add((uri, RDFS.subClassOf, parent))
    if comment:
        g.add((uri, RDFS.comment, Literal(comment)))

def define_obj_prop(uri, label, domain=None, range_=None, parent=None):
    """Helper: declare an OWL ObjectProperty."""
    g.add((uri, RDF.type,   OWL.ObjectProperty))
    g.add((uri, RDFS.label, Literal(label)))
    if domain:  g.add((uri, RDFS.domain, domain))
    if range_:  g.add((uri, RDFS.range,  range_))
    if parent:  g.add((uri, RDFS.subPropertyOf, parent))

def define_data_prop(uri, label, domain=None, range_=XSD.string):
    """Helper: declare an OWL DatatypeProperty."""
    g.add((uri, RDF.type,   OWL.DatatypeProperty))
    g.add((uri, RDFS.label, Literal(label)))
    if domain:  g.add((uri, RDFS.domain, domain))
    g.add((uri, RDFS.range,  range_))

# ── REA Base Classes ──────────────────────────────────────────────────────────
define_class(REA.Resource,   "Economic Resource",
             "Something of economic value.")
define_class(REA.Event,      "Economic Event",
             "An occurrence that changes resource values.")
define_class(REA.Agent,      "Economic Agent",
             "A party that participates in events.")
define_class(REA.Commitment, "Economic Commitment",
             "A promise to perform a future event.")

# ── Banking Resource Subclasses ───────────────────────────────────────────────
define_class(BANK.CashResource,     "Cash",            parent=REA.Resource)
define_class(BANK.LoanReceivable,   "Loan Receivable", parent=REA.Resource)
define_class(BANK.DepositLiability, "Deposit Liability", parent=REA.Resource)
define_class(BANK.InterestIncome,   "Interest Income", parent=REA.Resource)
define_class(BANK.Collateral,       "Collateral",      parent=REA.Resource)

# ── Banking Event Subclasses ──────────────────────────────────────────────────
define_class(BANK.LoanDisbursement, "Loan Disbursement", parent=REA.Event)
define_class(BANK.LoanRepayment,    "Loan Repayment",    parent=REA.Event)
define_class(BANK.InterestPayment,  "Interest Payment",  parent=REA.Event)
define_class(BANK.DepositReceived,  "Deposit Received",  parent=REA.Event)
define_class(BANK.Withdrawal,       "Withdrawal",        parent=REA.Event)

# ── Banking Agent Subclasses ──────────────────────────────────────────────────
define_class(BANK.RetailCustomer,    "Retail Customer",    parent=REA.Agent)
define_class(BANK.CorporateCustomer, "Corporate Customer", parent=REA.Agent)
define_class(BANK.BankEntity,        "Bank Entity",        parent=REA.Agent)
define_class(BANK.LoanOfficer,       "Loan Officer",       parent=REA.Agent)

7.3 Define Properties

# ── REA Object Properties ─────────────────────────────────────────────────────
define_obj_prop(REA.duality,         "duality",
                domain=REA.Event, range_=REA.Event)
define_obj_prop(REA.participatesIn,  "participates in",
                domain=REA.Agent, range_=REA.Event)
define_obj_prop(REA.asProvider,      "as provider",
                domain=REA.Agent, range_=REA.Event,
                parent=REA.participatesIn)
define_obj_prop(REA.asReceiver,      "as receiver",
                domain=REA.Agent, range_=REA.Event,
                parent=REA.participatesIn)
define_obj_prop(REA.decrements,      "decrements",
                domain=REA.Event, range_=REA.Resource)
define_obj_prop(REA.increments,      "increments",
                domain=REA.Event, range_=REA.Resource)
define_obj_prop(REA.fulfilledBy,     "fulfilled by",
                domain=REA.Commitment, range_=REA.Event)
define_obj_prop(REA.reservesResource,"reserves resource",
                domain=REA.Commitment, range_=REA.Resource)

# ── Datatype Properties ───────────────────────────────────────────────────────
define_data_prop(REA.quantity,      "quantity",
                 domain=REA.Resource, range_=XSD.decimal)
define_data_prop(REA.eventDate,     "event date",
                 domain=REA.Event,    range_=XSD.date)
define_data_prop(REA.amount,        "amount",
                 domain=REA.Event,    range_=XSD.decimal)
define_data_prop(REA.currency,      "currency",    range_=XSD.string)
define_data_prop(BANK.interestRate, "interest rate",
                 domain=BANK.LoanReceivable, range_=XSD.decimal)
define_data_prop(BANK.maturityDate, "maturity date",
                 domain=BANK.LoanReceivable, range_=XSD.date)
define_data_prop(BANK.loanId,       "loan ID",     range_=XSD.string)

7.4 Create Instance Data

# ── Helper: add an individual ─────────────────────────────────────────────────
def add_individual(uri, rdf_type, label):
    g.add((uri, RDF.type,   rdf_type))
    g.add((uri, RDFS.label, Literal(label)))
    return uri

# Agents
customer = add_individual(INST.customer_john_doe, BANK.RetailCustomer, "John Doe")
bank     = add_individual(INST.first_national_bank, BANK.BankEntity,   "First National Bank")
officer  = add_individual(INST.officer_jane_smith, BANK.LoanOfficer,   "Jane Smith")

# Resources
cash = add_individual(INST.bank_cash_usd, BANK.CashResource, "Bank USD Cash Pool")
g.add((cash, REA.quantity, Literal("50000000.00", datatype=XSD.decimal)))
g.add((cash, REA.currency, Literal("USD")))

loan_recv = add_individual(INST.loan_receivable_001, BANK.LoanReceivable,
                           "Personal Loan #L-2026-001")
g.add((loan_recv, BANK.loanId,       Literal("L-2026-001")))
g.add((loan_recv, REA.quantity,      Literal("25000.00",    datatype=XSD.decimal)))
g.add((loan_recv, REA.currency,      Literal("USD")))
g.add((loan_recv, BANK.interestRate, Literal("0.0875",      datatype=XSD.decimal)))
g.add((loan_recv, BANK.maturityDate, Literal("2031-06-11",  datatype=XSD.date)))

# Disbursement Event
disbursement = add_individual(INST.disbursement_001, BANK.LoanDisbursement,
                              "Disbursement for Loan L-2026-001")
g.add((disbursement, REA.eventDate,  Literal("2026-06-11", datatype=XSD.date)))
g.add((disbursement, REA.amount,     Literal("25000.00",   datatype=XSD.decimal)))
g.add((disbursement, REA.currency,   Literal("USD")))
g.add((disbursement, REA.decrements, cash))
g.add((disbursement, REA.increments, loan_recv))
g.add((disbursement, REA.asProvider, bank))
g.add((disbursement, REA.asReceiver, customer))

# Repayment Event
repayment = add_individual(INST.repayment_001a, BANK.LoanRepayment,
                           "Repayment 1 for Loan L-2026-001")
g.add((repayment, REA.eventDate,  Literal("2026-07-11", datatype=XSD.date)))
g.add((repayment, REA.amount,     Literal("516.31",     datatype=XSD.decimal)))
g.add((repayment, REA.currency,   Literal("USD")))
g.add((repayment, REA.decrements, loan_recv))
g.add((repayment, REA.increments, cash))
g.add((repayment, REA.asProvider, customer))
g.add((repayment, REA.asReceiver, bank))

# Duality link
g.add((disbursement, REA.duality, repayment))

7.5 Serialize and Query

# ── Serialize to Turtle ───────────────────────────────────────────────────────
turtle_output = g.serialize(format="turtle")
with open("bank_rea_ontology.ttl", "w") as f:
    f.write(turtle_output)
print(f"Ontology written: {len(g)} triples")

# ── SPARQL Query: find all events for a given customer ────────────────────────
from rdflib.plugins.sparql import prepareQuery

query = prepareQuery("""
    PREFIX rea:  <http://superml.dev/ontology/rea#>
    PREFIX bank: <http://superml.dev/ontology/bank#>
    PREFIX inst: <http://superml.dev/data/bank#>
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

    SELECT ?eventLabel ?eventDate ?amount ?role
    WHERE {
        ?event rea:eventDate ?eventDate ;
               rea:amount    ?amount ;
               rdfs:label    ?eventLabel .
        {
            ?event rea:asProvider inst:customer_john_doe .
            BIND("provider" AS ?role)
        } UNION {
            ?event rea:asReceiver inst:customer_john_doe .
            BIND("receiver" AS ?role)
        }
    }
    ORDER BY ?eventDate
""", initNs={"rea": REA, "bank": BANK, "inst": INST})

print("\nEvents involving John Doe:")
print(f"{'Event':<40} {'Date':<12} {'Amount':>10} {'Role'}")
print("-" * 75)
for row in g.query(query):
    print(f"{str(row.eventLabel):<40} {str(row.eventDate):<12} "
          f"{str(row.amount):>10} {str(row.role)}")

Expected output:

Ontology written: 74 triples

Events involving John Doe:
Event                                    Date         Amount   Role
---------------------------------------------------------------------------
Disbursement for Loan L-2026-001         2026-06-11    25000.00 receiver
Repayment 1 for Loan L-2026-001          2026-07-11      516.31 provider

7.6 Query the Duality Graph

# ── SPARQL Query: trace the duality chain for a loan ──────────────────────────
duality_query = prepareQuery("""
    PREFIX rea:  <http://superml.dev/ontology/rea#>
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

    SELECT ?giveLabel ?receiveLabel ?amount
    WHERE {
        ?giveEvent rea:duality ?receiveEvent ;
                   rdfs:label  ?giveLabel ;
                   rea:amount  ?amount .
        ?receiveEvent rdfs:label ?receiveLabel .
    }
""")

print("\nDuality pairs (economic exchanges):")
for row in g.query(duality_query):
    print(f"  GIVE: {row.giveLabel}")
    print(f"  RECEIVE: {row.receiveLabel}")
    print(f"  AMOUNT: {row.amount}")

Part 8 — Real-World Example: Modelling a Loan Lifecycle

A loan doesn’t happen in a single event. It has a lifecycle — and REA, with commitments, models the whole thing. Here’s the complete lifecycle mapped to REA:

PHASE 1 — APPLICATION (Commitment)
─────────────────────────────────────────────────────────
  LoanApplicationCommitment
    ├── involves Agent: customer (obligated to provide documentation)
    ├── involves Agent: bank (obligated to make a decision)
    └── reservesResource: bank's lending capacity

PHASE 2 — APPROVAL (Commitment fulfilled by Commitment)
─────────────────────────────────────────────────────────
  LoanApprovalCommitment
    ├── fulfilledBy ← LoanApplicationCommitment
    ├── involves Agent: bank (commits to disburse)
    ├── involves Agent: customer (commits to repay schedule)
    └── reservesResource: LoanReceivable (prospective)

PHASE 3 — COLLATERAL PLEDGE (Event)
─────────────────────────────────────────────────────────
  CollateralPledgeEvent
    ├── decrements: customer's CollateralAsset
    ├── increments: bank's CollateralHolding
    ├── asProvider: customer
    └── asReceiver: bank

PHASE 4 — DISBURSEMENT (Event — fulfils ApprovalCommitment)
─────────────────────────────────────────────────────────
  LoanDisbursementEvent
    ├── decrements: CashResource
    ├── increments: LoanReceivable
    ├── asProvider: bank
    ├── asReceiver: customer
    └── duality → RepaymentScheduleEvents (future)

PHASE 5 — REPAYMENTS (Recurring Events)
─────────────────────────────────────────────────────────
  LoanRepaymentEvent (×60 for a 5-year loan)
    ├── decrements: LoanReceivable (principal portion)
    ├── increments: CashResource
    ├── asProvider: customer
    └── asReceiver: bank

  InterestPaymentEvent (×60)
    ├── decrements: AccruedInterestReceivable
    ├── increments: InterestIncome
    ├── asProvider: customer
    └── asReceiver: bank
    └── duality → InterestAccrualEvent

PHASE 6 — MATURITY / CLOSE (Event)
─────────────────────────────────────────────────────────
  CollateralReleaseEvent
    ├── decrements: bank's CollateralHolding
    ├── increments: customer's CollateralAsset
    ├── asProvider: bank
    └── asReceiver: customer

This complete lifecycle lives in the ontology as a connected graph. Any query — “what is the total interest paid by this customer?”, “what collateral backs this exposure?”, “which loan officer approved the largest disbursements last quarter?” — is a SPARQL traversal over the same graph, with no ETL required.


Part 9 — REA Meets FIBO: The Industry Standard Layer

FIBO (Financial Industry Business Ontology) is the OMG/EDM Council standard ontology for financial services. It has hundreds of classes covering contracts, parties, currencies, products, and regulations. Its transaction and event layers align closely with REA.

Key FIBO namespaces you’ll encounter:

FIBO ModuleFIBO PrefixREA Equivalent
fibo-fnd-acc-curCurrencies, amountsREA scalar attributes
fibo-fnd-pas-pasParties and situationsrea:Agent
fibo-fbc-fi-fiFinancial instrumentsrea:Resource subclasses
fibo-fbc-dae-dbtDebt instruments (loans)bank:LoanReceivable
fibo-fnd-rel-relCore relationsrea:duality, rea:participatesIn
fibo-sec-sec-lstSecurities listingsbank:SecurityPosition

Mapping your REA ontology to FIBO

@prefix fibo-fbc-dae-dbt: <https://spec.edmcouncil.org/fibo/ontology/FBC/DebtAndEquities/Debt/> .
@prefix fibo-fnd-pas-pas: <https://spec.edmcouncil.org/fibo/ontology/FND/Parties/Parties/> .

# Declare that our LoanReceivable is a subclass of FIBO's Loan
bank:LoanReceivable
    rdfs:subClassOf fibo-fbc-dae-dbt:Loan ;
    owl:equivalentClass fibo-fbc-dae-dbt:TermLoan .

# Declare that our RetailCustomer maps to FIBO's PartyInRole
bank:RetailCustomer
    rdfs:subClassOf fibo-fnd-pas-pas:PartyInRole .

This alignment means your REA ontology can interoperate directly with FIBO-based regulatory reporting tools, XBRL taxonomies, and data products published by financial data vendors.


Part 10 — REA as a Foundation for AI/ML in Banking

10.1 Why LLMs Need Ontologies

A language model querying a banking database faces a fundamental ambiguity problem: loan_bal, loan_balance_amt, principal_outstanding — these all mean the same thing in different systems. Without a semantic layer, the model either hallucinates a mapping or requires extensive prompt engineering per system.

An REA ontology solves this by providing a canonical vocabulary:

# Without REA ontology: LLM must guess column meanings
sql = "SELECT loan_bal FROM tbl_core_lending WHERE cust_id = ?"
# Ambiguous: is loan_bal the original principal or the current balance?

# With REA ontology: the question is expressed in terms of instances
sparql = """
    SELECT (SUM(?amount) AS ?totalRepaid)
    WHERE {
        ?event a bank:LoanRepayment ;
               rea:amount     ?amount ;
               rea:asProvider inst:customer_john_doe .
    }
"""
# Unambiguous: this asks for the sum of all repayment event amounts
# where the customer is the provider

10.2 Building an REA-Grounded Agent

Here’s how to wire an REA ontology into an LLM-based banking agent using LangChain and RDFLib:

from rdflib import Graph
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

# Load the ontology at startup
g = Graph()
g.parse("bank_rea_ontology.ttl", format="turtle")

@tool
def query_customer_events(customer_id: str, event_type: str = None) -> str:
    """
    Query all economic events involving a specific customer.
    Returns a summary of events, dates, and amounts.
    event_type options: 'LoanDisbursement', 'LoanRepayment',
                        'InterestPayment', 'DepositReceived', 'Withdrawal'
    """
    type_filter = ""
    if event_type:
        type_filter = f"?event a bank:{event_type} ."

    sparql = f"""
        PREFIX rea:  <http://superml.dev/ontology/rea#>
        PREFIX bank: <http://superml.dev/ontology/bank#>
        PREFIX inst: <http://superml.dev/data/bank#>
        PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

        SELECT ?label ?date ?amount ?role
        WHERE {{
            {type_filter}
            ?event rdfs:label ?label ;
                   rea:eventDate ?date ;
                   rea:amount    ?amount .
            {{
                ?event rea:asProvider inst:{customer_id} .
                BIND("provider" AS ?role)
            }} UNION {{
                ?event rea:asReceiver inst:{customer_id} .
                BIND("receiver" AS ?role)
            }}
        }}
        ORDER BY ?date
    """
    results = list(g.query(sparql))
    if not results:
        return f"No events found for customer {customer_id}."
    lines = [f"{r.label} | {r.date} | ${r.amount} | {r.role}"
             for r in results]
    return "\n".join(lines)

@tool
def get_loan_duality_chain(loan_id: str) -> str:
    """
    Retrieve the economic exchange chain (duality) for a specific loan.
    Shows disbursement linked to all repayments.
    """
    sparql = f"""
        PREFIX rea:  <http://superml.dev/ontology/rea#>
        PREFIX bank: <http://superml.dev/ontology/bank#>
        PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

        SELECT ?disburseLabel ?repayLabel ?repayDate ?repayAmount
        WHERE {{
            ?disbursement a bank:LoanDisbursement ;
                          rdfs:label   ?disburseLabel ;
                          rea:duality  ?repayment .
            ?repayment rdfs:label ?repayLabel ;
                       rea:eventDate ?repayDate ;
                       rea:amount    ?repayAmount .
        }}
        ORDER BY ?repayDate
    """
    results = list(g.query(sparql))
    if not results:
        return f"No duality chain found for loan {loan_id}."
    lines = [f"GIVE: {r.disburseLabel} <-> RECEIVE: {r.repayLabel} "
             f"on {r.repayDate} for ${r.repayAmount}"
             for r in results]
    return "\n".join(lines)

# Wire into an agent
llm = ChatOpenAI(model="gpt-4o", temperature=0)
agent = llm.bind_tools([query_customer_events, get_loan_duality_chain])

10.3 REA for Fraud Detection

REA’s event graph is a natural foundation for fraud detection because it captures relationships between agents and events — exactly what rule-based systems and graph neural networks need.

A fraud ring in REA looks like this: multiple Agent instances connected through a web of Event instances, all funnelling resources toward a single beneficiary. The ontology makes that pattern queryable:

PREFIX rea: <http://superml.dev/ontology/rea#>

# Find agents who are receivers in unusually many events within 24 hours
SELECT ?receiver (COUNT(?event) AS ?eventCount) (SUM(?amount) AS ?totalReceived)
WHERE {
    ?event a rea:Event ;
           rea:eventDate ?date ;
           rea:amount    ?amount ;
           rea:asReceiver ?receiver .
    FILTER (?date >= "2026-06-10"^^xsd:date &&
            ?date <= "2026-06-11"^^xsd:date)
}
GROUP BY ?receiver
HAVING (COUNT(?event) > 10)
ORDER BY DESC(?totalReceived)

Part 11 — Exercises

Work through these to solidify your understanding. Suggested solutions are in comments.

Exercise 1: Model a Wire Transfer

A wire transfer involves two accounts at the same bank. Map it to REA.

Questions to answer:

  • What are the Resources? (Hint: two deposit liabilities)
  • What is the Event?
  • Who are the Agents?
  • What does the duality look like for an outgoing vs. incoming wire?
  • Write the Turtle instance data.
# Your answer here:
# inst:wire_event_001 a bank:WireTransfer ;
#     rea:decrements  inst:sender_deposit_liability ;
#     rea:increments  inst:receiver_deposit_liability ;
#     rea:asProvider  inst:sender_customer ;
#     rea:asReceiver  inst:receiver_customer ;
#     ...

Exercise 2: Model a Securities Trade

A bank’s trading desk buys $500,000 worth of 10-year Treasury bonds.

Questions to answer:

  • What Resources are involved? (Hint: cash and a security position)
  • What Events? (disbursement of cash, receipt of securities)
  • What is the duality? (payment event ↔ delivery event)
  • How would you model the settlement lag (T+1)?
  • Write the Turtle instance data and a SPARQL query to find the trade.
# Resources
# inst:treasury_position_001 a bank:SecurityPosition ;
#     bank:cusip    "912828YK0" ;
#     bank:faceValue "500000.00"^^xsd:decimal ;
#     ...

# Events
# inst:cash_payment_001 a bank:CashPayment ;
#     rea:decrements inst:bank_cash_usd ;
#     rea:increments inst:broker_cash_claim ;
#     rea:duality    inst:securities_delivery_001 ;
#     ...

Exercise 3: Add a Commitment Layer

Extend the loan example from Part 6 with the commitment lifecycle. Specifically:

  1. Create a LoanApplicationCommitment instance
  2. Create a LoanApprovalCommitment that fulfils the application commitment
  3. Link the LoanDisbursement event as fulfilling the approval commitment
  4. Add a SPARQL query that retrieves the full commitment-to-event chain for a given loan

Exercise 4: SPARQL — Interest Accrual Report

Write a SPARQL query over the graph from Part 7 that returns:

  • The loan ID
  • Total principal disbursed
  • Total principal repaid so far
  • Outstanding principal balance (disbursed minus repaid)
PREFIX rea:  <http://superml.dev/ontology/rea#>
PREFIX bank: <http://superml.dev/ontology/bank#>

SELECT ?loanId 
       (SUM(?disbursedAmount) AS ?totalDisbursed)
       (SUM(?repaidAmount) AS ?totalRepaid)
       # How would you compute the outstanding balance?
WHERE {
    # Your triple patterns here
}
GROUP BY ?loanId

Exercise 5: Extend for Regulatory Reporting

The OCC requires banks to report loans by risk weight category. Add the following to the ontology:

  1. A RiskWeightCategory class with individuals (e.g., RiskWeight100, RiskWeight50)
  2. An owl:ObjectProperty called bank:riskWeight linking LoanReceivable to RiskWeightCategory
  3. Instance data assigning risk weights to the loan from Part 6
  4. A SPARQL query that groups outstanding loan balances by risk weight (the basis of a simplified RWA calculation)

Further Reading

Original Papers

  • McCarthy, W.E. (1982). The REA Accounting Model: A Generalized Framework for Accounting Systems in a Shared Data Environment. The Accounting Review, 57(3), 554–578.
  • Geerts, G.L. & McCarthy, W.E. (2002). An Ontological Analysis of the Primitives of the REA Enterprise Information Architecture. International Journal of Accounting Information Systems.

Standards and Ontologies

Tools

  • RDFLib (Python) — build and query RDF/OWL graphs in Python
  • Protégé — open-source GUI ontology editor; great for visualizing class hierarchies
  • Apache Jena — Java-based RDF/SPARQL platform, widely used in enterprise settings
  • Stardog / GraphDB — production-grade RDF stores with SPARQL endpoints

Banking Ontology Applied


Summary

ConceptOne-Line Definition
REAA model that records economic reality as Resources, Events, and Agents
ResourceSomething of economic value (cash, loan, deposit, security)
EventSomething that happened and changed resource quantities
AgentA party who participated in an event as provider or receiver
DualityThe link between the give-event and the receive-event in an exchange
ParticipationThe link between an agent and the event they were part of
StockflowThe link between an event and the resources it increments or decrements
CommitmentA promise to perform a future event (loan approval, repayment schedule)
OWL OntologyA formal, machine-readable version of REA using RDF/OWL syntax
FIBOThe industry standard banking ontology that builds on REA principles

The core insight of REA is the one McCarthy had in 1982: don’t record accounting entries — record the economic events that generate them. In banking today, that insight is foundational to semantic data layers, regulatory reporting, AI-driven decisioning, and anything else that requires machines to understand what financial transactions mean, not just what numbers they produced.

Enterprise AI Architecture

Want more enterprise AI architecture breakdowns?

Subscribe to SuperML.

Comments

Sign in to leave a comment

Back to Blog

Related Posts

View All Posts »

Why Fraud Rings Survive XGBoost — and How GNNs Stop Them

Row-based ML catches individual bad actors but misses coordinated fraud rings. Graph Neural Networks propagate relational context through transaction networks — here's the architecture, the PyTorch Geometric code, and the production gotchas that matter more than model choice.