Blog Analysis & Technical Rebuild

Operating Structure: Neo4j vs. Virtuoso

Morgan Sénéchal's Neo4j Chapter 1 post models who-owns-what as a property graph — Organisation, BusinessFunction, Role, and Person nodes in Cypher 25. This collection rebuilds the identical AcmeBank example as SQL tables exposed through Virtuoso RDF Views, runs SPARQL equivalents of its three worked queries, then shows agent-rdf-memory already running the same pattern on a live local Virtuoso instance, for Kingsley Uyi Idehen.

KG curated by the kg-generator skill on behalf of Kingsley Uyi Idehen.

Synopsis

One Org Chart, Two Query Engines

Neo4j's Operating Structure chapter argues that AI agents cannot reliably answer “who owns what” from stale org charts and tribal knowledge, and models organisational accountability as a property graph queried in Cypher on Neo4j Enterprise Edition.

This collection rebuilds the identical AcmeBank example — the same five entity types, the same five relationships, the same reporting chain from SMB Loan Underwriter to CEO — as ordinary SQL tables exposed through Virtuoso RDF Views, runs SPARQL equivalents of all three of the article's worked queries, and adds what a single property-graph instance cannot do without an import step: federated cross-endpoint SPARQL. It then turns the same idea back on the agent that wrote this comparison — agent-rdf-memory already runs this pattern today, on a live local Virtuoso instance, to answer not “who owns SMB Lending” but “which behavioral rule governs this task.” This is not just a described approach: the schema, sample data, Quad Map, and a fully cross-referenced ontology were carried unchanged onto demo.openlinksw.com, a public Virtuoso server, after being built and proven on a development instance — with every AcmeBank role and person resource dereferenceable there as real Linked Data.

Neo4j's Model

Five Node Types, Five Relationships

Chapter 1 models AcmeBank as five node types — Organisation, BusinessOrganisation, BusinessFunction, Role, and Person — joined by five relationships: PART_OF, HAS_FUNCTION, SCOPED_TO, REPORTS_TO, and FILLED_BY. Only PART_OF, SCOPED_TO, and FILLED_BY carry validity dates — a deliberate choice, since restructuring only actually happens on those three edges.

🏦

Organisation

The enterprise entity itself — AcmeBank — at the root of the graph.

📋

BusinessFunction

A named activity such as SMB Lending, anchoring later layers of the series.

🏷️

Role

A position with a title and seniority, scoped to a function, reporting to another role.

👤

Person

Fills a Role via FILLED_BY — an edge the article notes silently also accepts an Agent node.

AcmeBank's worked example

Modeled to three levels of depth — bank → six business units → twelve teams — rather than mirroring HR data verbatim. The concrete Cypher load ships 18 organisations, 10 business functions, 32 roles, and 58 people. Eleanor Fairbairn held Head of SMB Lending from 2023-01-01 to 2026-01-01; Yusuf Rahman has held it since — the article's example of FILLED_BY validity dates making a tribal-knowledge gap explicit instead of hiding it.

Comparison

Eleven Dimensions, Side by Side

Each row below is its own described entity in the companion RDF — click a dimension name to look it up.

DimensionNeo4j (Chapter 1)Virtuoso alternative
Data modelA dedicated property graph — labeled nodes and relationships with properties.Relational tables and an RDF quad store as co-equal models in one engine, so the same facts stay queryable in SQL and SPARQL simultaneously.
Query languageCypher 25, requiring Neo4j 2026.02+ Enterprise Edition.SPARQL over the RDF View, plain SQL over the same tables, or SPASQL mixing both — no proprietary graph dialect required.
Schema declarationAn enforced “graph type” naming node types, properties, and allowed relationships — an Enterprise-only feature.Ordinary SQL DDL constraints plus an OWL ontology the RDF View maps into, with SHACL available for stricter shapes.
Reusing data that already existsHR/identity-provider data must be extracted and loaded through a governed ingestion pipeline before it is queryable.RDF Views (Quad Map Patterns / R2RML) expose the same live rows as a virtual RDF graph with zero copying — SPARQL compiles back to SQL at query time.
Entity identityInternal node identifiers, looked up by property match within one database.Every Role, Person, and BusinessFunction is a dereferenceable HTTP IRI — identity is a hyperlink usable outside the database that minted it.
Cross-source federationQueries run against one Neo4j instance or cluster; joining another org's data means importing it first.SPARQL-FED's SERVICE clause joins the local RDF View against any remote SPARQL endpoint in one federated query — no import step.
Modeling an AI agent as a role fillerFILLED_BY is left untyped, silently accepting either a Person or an Agent node, without recording which agent acted on whose authority.PROV-O typing (prov:Person, prov:SoftwareAgent, prov:actedOnBehalfOf) makes the filler's kind and delegation explicit and queryable.
Temporal validity of relationshipsValidity dates are hand-picked onto exactly three relationship types where restructuring actually happens.The identical selective pattern, as ordinary datatype columns/properties — generalizes to any future relationship without a schema migration.
Governance and provenanceEntity resolution, provenance stamping, and schema validation are custom pipeline steps performed on every write.PROV-O is a first-class W3C data model from the outset — provenance is queried with the same SPARQL used for everything else.
Licensing and standards longevityThis chapter's stated requirement is Neo4j 2026.02+ Enterprise Edition.RDF (1999), SPARQL (2008), and R2RML (2012) are W3C standards implementable by any compliant engine; Virtuoso ships an open-source edition.
Does the platform reuse this pattern for itself?Agent tooling and memory are deferred to chapters 6–7 of the same series, described as future work.agent-rdf-memory already runs the identical “queryable structure consulted before acting” pattern today, against a live local Virtuoso endpoint.
Data modelNeo4j

A dedicated property graph — labeled nodes and relationships with properties.

Data modelVirtuoso

Relational tables and an RDF quad store as co-equal models in one engine, so the same facts stay queryable in SQL and SPARQL simultaneously.

Query languageNeo4j

Cypher 25, requiring Neo4j 2026.02+ Enterprise Edition.

Query languageVirtuoso

SPARQL over the RDF View, plain SQL over the same tables, or SPASQL mixing both — no proprietary graph dialect required.

Schema declarationNeo4j

An enforced “graph type” naming node types, properties, and allowed relationships — an Enterprise-only feature.

Schema declarationVirtuoso

Ordinary SQL DDL constraints plus an OWL ontology the RDF View maps into, with SHACL available for stricter shapes.

Reusing data that already existsVirtuoso

RDF Views (Quad Map Patterns / R2RML) expose the same live rows as a virtual RDF graph with zero copying — SPARQL compiles back to SQL at query time.

Entity identityNeo4j

Internal node identifiers, looked up by property match within one database.

Entity identityVirtuoso

Every Role, Person, and BusinessFunction is a dereferenceable HTTP IRI — identity is a hyperlink usable outside the database that minted it.

Cross-source federationNeo4j

Queries run against one Neo4j instance or cluster; joining another org's data means importing it first.

Cross-source federationVirtuoso

SPARQL-FED's SERVICE clause joins the local RDF View against any remote SPARQL endpoint in one federated query — no import step.

Temporal validity of relationshipsVirtuoso

The identical selective pattern, as ordinary datatype columns/properties — generalizes to any future relationship without a schema migration.

Governance and provenanceNeo4j

Entity resolution, provenance stamping, and schema validation are custom pipeline steps performed on every write.

Governance and provenanceVirtuoso

PROV-O is a first-class W3C data model from the outset — provenance is queried with the same SPARQL used for everything else.

Licensing and standards longevityVirtuoso

RDF (1999), SPARQL (2008), and R2RML (2012) are W3C standards implementable by any compliant engine; Virtuoso ships an open-source edition.

Virtuoso Alternative

Same Model, SQL Tables, Zero-Copy RDF

The Virtuoso rebuild keeps AcmeBank as five ordinary relational tables, then declares Virtuoso RDF Views (Quad Map Patterns, the native equivalent of an R2RML mapping) over them — a zero-copy virtual RDF graph the SPARQL processor compiles back to SQL at query time, live against the same rows Cypher would otherwise need ETL'd into Neo4j. The role/person/assignment facts that RDF View would produce are asserted directly in this document's own companion graph below, so every SPARQL recipe in this section runs live against the SAME graph as the rest of this document.

SQL schema and sample data

SQL

organisation table (DDL)

CREATE TABLE organisation (
  org_id       INTEGER PRIMARY KEY,
  org_name     VARCHAR(120) NOT NULL,
  parent_org_id INTEGER REFERENCES organisation(org_id),
  valid_from   DATE,
  valid_to     DATE
);
SQL

business_function table (DDL)

CREATE TABLE business_function (
  function_id  INTEGER PRIMARY KEY,
  function_name VARCHAR(120) NOT NULL,
  org_id       INTEGER NOT NULL REFERENCES organisation(org_id)
);
SQL

org_role table (DDL)

CREATE TABLE org_role (
  role_id      INTEGER PRIMARY KEY,
  role_title   VARCHAR(120) NOT NULL,
  seniority    VARCHAR(40),
  function_id  INTEGER NOT NULL REFERENCES business_function(function_id),
  reports_to_role_id INTEGER REFERENCES org_role(role_id),
  scoped_from  DATE,
  scoped_to    DATE
);
SQL

person table (DDL)

CREATE TABLE person (
  person_id    INTEGER PRIMARY KEY,
  full_name    VARCHAR(160) NOT NULL,
  filler_kind  VARCHAR(20) DEFAULT 'Person'
    CHECK (filler_kind IN ('Person','Agent'))
);
SQL

role_assignment table (DDL) — the FILLED_BY equivalent

Carries the validity window on the fill itself rather than on the person, so the same person can fill a role twice non-contiguously and the history stays exact.

CREATE TABLE role_assignment (
  assignment_id INTEGER PRIMARY KEY,
  role_id      INTEGER NOT NULL REFERENCES org_role(role_id),
  person_id    INTEGER NOT NULL REFERENCES person(person_id),
  filled_from  DATE NOT NULL,
  filled_to    DATE
);
SQL

Sample data — the AcmeBank reporting chain and the Eleanor/Yusuf succession

A small, directly comparable slice of the AcmeBank dataset — just enough rows to reproduce the article's three worked query results (current accountability, historical holders, reporting chain) against Virtuoso instead of Neo4j.

INSERT INTO organisation (org_id, org_name, parent_org_id) VALUES
  (1, 'AcmeBank', NULL),
  (2, 'SMB Banking', 1);

INSERT INTO business_function (function_id, function_name, org_id) VALUES
  (1, 'SMB Lending', 2);

INSERT INTO org_role (role_id, role_title, seniority, function_id, reports_to_role_id) VALUES
  (1, 'CEO', 'Executive', 1, NULL),
  (2, 'Head of SMB Banking', 'Senior', 1, 1),
  (3, 'Head of SMB Lending', 'Senior', 1, 2),
  (4, 'SMB Loan Underwriter', 'Individual Contributor', 1, 3);

INSERT INTO person (person_id, full_name) VALUES
  (1, 'Eleanor Fairbairn'),
  (2, 'Yusuf Rahman');

INSERT INTO role_assignment (assignment_id, role_id, person_id, filled_from, filled_to) VALUES
  (1, 3, 1, '2023-01-01', '2026-01-01'),  -- Eleanor Fairbairn, Head of SMB Lending (past)
  (2, 3, 2, '2026-01-01', NULL);          -- Yusuf Rahman, Head of SMB Lending (current)
SQL

RDF View declaration — mapping the tables to the org ontology

Virtuoso Quad Map Patterns bind each table row to an RDF resource and each foreign key to an object property, producing a virtual graph that is never materialized: every SPARQL query against it is compiled back to SQL against the live tables at query time. FULLY VERIFIED 2026-08-31 against a live local Virtuoso 8.3.3335 instance, after consulting docs.openlinksw.com directly (the correct form differs from the SUBJECT/PREDICATE pseudo-syntax shown in most illustrative examples, including earlier drafts of this one): the mapping goes inside ALTER QUAD STORAGE virtrdf:DefaultQuadStorage, not a separately-named CREATE QUAD MAP; every table used (including a second alias of the same table for a self-referencing FK like reports_to_role_id) is listed in a FROM clause before the mapping body; a self-join condition inside the body uses the ^{alias.}^.column escaped form, not plain alias.column; and every mapping's AS name must be a prefixed QName (virtrdf:name), not a bare identifier. All mapping declarations that write to the SAME target graph must be issued in ONE statement — a second ALTER QUAD STORAGE targeting that graph REPLACES the first one's mappings rather than adding to them, which is easy to miss since each statement reports success independently. Verified end to end: SELECT * over the resulting graph returns exactly the expected 11 triples, and both the current-accountability and historical-holders SPARQL recipes below return exactly the expected rows when run against this live mapping. One real limitation surfaced: SPARQL property paths (reportsTo+) and Virtuoso's native OPTION (TRANSITIVE, ...) both return zero rows over this RDF-View-backed predicate on this instance, even though the individual single-hop reportsTo triples are all present and directly queryable — recursive traversal over a virtual (SQL-compiled) predicate does not work the same way it does over materialized triples here. See :sparqlReportingChain for the resulting caveat. EXTENDED 2026-09-02: the Quad Map now also asserts rdf:type on every minted role/person/assignment resource (previously the view emitted only data/object-property triples, no class membership), and the full :orgOntology TBox — every term fully gate-compliant per ontology-cross-reference-gate.ttl and owl-property-characterization.ttl, with verified external cross-references to the W3C Organization Ontology (org:), schema.org, and PROV-O — is loaded into the same named graph. This entire deployment (schema, data, Quad Map, ontology) was also replicated on demo.openlinksw.com (via isql, account kidehen, tables under the DB.kidehen schema rather than DB.DBA since that account is not the instance dba), using demo-scoped entity IRIs anchored to the verified DAV collection https://demo.openlinksw.com/DAV/home/kidehen/rdf_sink/ while reusing the identical predicate/class vocabulary IRIs — the vocabulary is shared across deployments; only instance IRIs are deployment-scoped. PLATFORM DIFFERENCE found during verification: unlike the local instance, demo.openlinksw.com serves a site-wide 'Open this link?' click-through confirmation page in front of the rewrite target for a plain anonymous GET on /role/{id} — a human following the link in a browser just clicks Continue, but a non-interactive client (curl, a script) must POST h=1 to the same URL to reach the actual Faceted Browser description page. Verified end to end this way: /role/3 on demo.openlinksw.com resolves to 'Head of SMB Lending', rdf:type OrgRole, exactly as on the local instance.

SPARQL CREATE IRI CLASS <role_iri> "https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl#role/%d" (in role_id integer not null) ;
SPARQL CREATE IRI CLASS <person_iri> "https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl#person/%d" (in person_id integer not null) ;
SPARQL CREATE IRI CLASS <assignment_iri> "https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl#assignment/%d" (in assignment_id integer not null) ;

-- VERIFIED live 2026-08-31. All mappings for one target graph must be in ONE
-- ALTER QUAD STORAGE statement -- a second statement targeting the same
-- GRAPH replaces the first rather than adding to it.
SPARQL
ALTER QUAD STORAGE virtrdf:DefaultQuadStorage
  FROM DB.DBA.org_role AS role_tbl
  FROM DB.DBA.org_role AS role_tbl_1
  FROM DB.DBA.role_assignment AS ra_tbl
  FROM DB.DBA.person AS p_tbl
{
  GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl> {
    -- role_tbl_1 is a SECOND alias of the same org_role table, required for
    -- the reports_to_role_id self-referencing foreign key.
    <role_iri> (role_tbl.role_id)
      <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#OrgRole> as virtrdf:map_role_type ;
      <http://schema.org/name> role_tbl.role_title as virtrdf:map_role_name ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#reportsTo> <role_iri> (role_tbl_1.role_id)
        where ( ^{role_tbl.}^.reports_to_role_id = ^{role_tbl_1.}^.role_id ) as virtrdf:map_role_reportsTo .

    -- filledBy/filledFrom/filledTo are bound to assignment_iri (one per
    -- role_assignment row), never to role_iri directly: role_assignment can
    -- hold more than one row per role_id (a past holder and a current one),
    -- and binding these facts onto the shared role subject would let a
    -- SPARQL query pair an unrelated pair's filledTo date with the wrong
    -- filledBy person via cross-join.
    <assignment_iri> (ra_tbl.assignment_id)
      <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#RoleAssignment> as virtrdf:map_assignment_type ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#forRole> <role_iri> (ra_tbl.role_id) as virtrdf:map_forRole ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#filledBy> <person_iri> (ra_tbl.person_id) as virtrdf:map_filledBy ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#filledFrom> ra_tbl.filled_from as virtrdf:map_filledFrom ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#filledTo> ra_tbl.filled_to as virtrdf:map_filledTo .

    <person_iri> (p_tbl.person_id)
      <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#OrgPerson> as virtrdf:map_person_type ;
      <http://schema.org/name> p_tbl.full_name as virtrdf:map_person_name .
  }
} ;

-- The Quad Map above only instantiates the ABox (data). Load the TBox
-- (:orgOntology and its classes/properties, fully annotated per
-- ontology-cross-reference-gate.ttl and owl-property-characterization.ttl)
-- into the SAME graph so a DESCRIBE or type query returns complete term
-- definitions alongside the live data. VERIFIED live on both the local
-- instance and demo.openlinksw.com 2026-09-02.
SPARQL
PREFIX post: <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#>
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX schema: <http://schema.org/>
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
INSERT DATA {
  GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl> {

    post:orgOntology a owl:Ontology ;
      rdfs:label "AcmeBank Org Ontology (RDF View target)"@en ;
      rdfs:comment "Lightweight OWL vocabulary the AcmeBank RDF View (SQL tables plus a Virtuoso Quad Map) instantiates: one class per organizational resource type, one property per organizational relationship, loaded as TBox triples into the same named graph as the ABox instance data so a SPARQL DESCRIBE or type query returns complete term definitions alongside the live data."@en .

    post:OrgRole a owl:Class ;
      rdfs:label "Organizational Role"@en ;
      rdfs:comment "A named position in AcmeBank's reporting hierarchy (e.g. Head of SMB Lending), instantiated once per row of the acmebank_role table. Corresponds more precisely to org:Post (a specific, at-most-one-holder-at-a-time position with its own reportsTo chain) than to org:Role's more general classification sense; both are cross-referenced."@en ;
      rdfs:isDefinedBy post:orgOntology ;
      rdfs:seeAlso org:Post, org:Role .

    post:OrgPerson a owl:Class ;
      rdfs:label "Organizational Person"@en ;
      rdfs:comment "A human filler of an OrgRole, instantiated once per row of the acmebank_person table. Identical in extension to schema:Person for every instance this RDF View emits."@en ;
      rdfs:isDefinedBy post:orgOntology ;
      owl:equivalentClass schema:Person .

    post:RoleAssignment a owl:Class ;
      rdfs:label "Role Assignment"@en ;
      rdfs:comment "Reifies one 'person X filled role Y from date A to date B' fact as its own resource (forRole/filledBy/filledFrom/filledTo), instantiated once per row of the acmebank_role_assignment table, so a role with more than one historical holder can't have a query cross-join an unrelated holder's end date onto the wrong person."@en ;
      rdfs:isDefinedBy post:orgOntology ;
      owl:equivalentClass prov:Attribution ;
      rdfs:seeAlso schema:Role, org:Membership .

    post:reportsTo a owl:ObjectProperty, owl:AsymmetricProperty, owl:IrreflexiveProperty, owl:FunctionalProperty ;
      rdfs:label "reports to"@en ;
      rdfs:comment "Direct-line management edge from one OrgRole to the OrgRole immediately above it. Deliberately NOT owl:TransitiveProperty: reporting chains are walked with repeated single-hop queries or a SPARQL property path over materialized data, never asserted as a single transitive fact -- and on this RDF-View-backed (virtual) predicate, property-path and OPTION(TRANSITIVE,...) traversal itself returns zero rows on this Virtuoso instance."@en ;
      rdfs:domain post:OrgRole ;
      rdfs:range post:OrgRole ;
      rdfs:isDefinedBy post:orgOntology ;
      rdfs:subPropertyOf org:reportsTo .

    post:forRole a owl:ObjectProperty, owl:AsymmetricProperty, owl:IrreflexiveProperty, owl:FunctionalProperty ;
      rdfs:label "for role"@en ;
      rdfs:comment "The OrgRole a given RoleAssignment fills. Each assignment is for exactly one role."@en ;
      rdfs:domain post:RoleAssignment ;
      rdfs:range post:OrgRole ;
      rdfs:isDefinedBy post:orgOntology ;
      rdfs:seeAlso org:role .

    post:filledBy a owl:ObjectProperty, owl:AsymmetricProperty, owl:IrreflexiveProperty, owl:FunctionalProperty ;
      rdfs:label "filled by"@en ;
      rdfs:comment "The OrgPerson who holds a given RoleAssignment. Each assignment has exactly one filler."@en ;
      rdfs:domain post:RoleAssignment ;
      rdfs:range post:OrgPerson ;
      rdfs:isDefinedBy post:orgOntology ;
      rdfs:subPropertyOf org:member .

    post:filledFrom a owl:DatatypeProperty, owl:FunctionalProperty ;
      rdfs:label "filled from"@en ;
      rdfs:comment "The date a RoleAssignment began."@en ;
      rdfs:domain post:RoleAssignment ;
      rdfs:range xsd:date ;
      rdfs:isDefinedBy post:orgOntology ;
      rdfs:subPropertyOf schema:startDate .

    post:filledTo a owl:DatatypeProperty, owl:FunctionalProperty ;
      rdfs:label "filled to"@en ;
      rdfs:comment "The date a RoleAssignment ended; absent for the current holder."@en ;
      rdfs:domain post:RoleAssignment ;
      rdfs:range xsd:date ;
      rdfs:isDefinedBy post:orgOntology ;
      rdfs:subPropertyOf schema:endDate .
  }
} ;
SQL

Batch script — acmebank.sql, ready to run against a Virtuoso instance

The five table DDL statements, the sample INSERTs, and the RDF View declaration above, concatenated in execution order into one script — copy this single card to acmebank.sql and run it with isql (or paste into Virtuoso Conductor's Interactive SQL) to actually create the schema, load the data, and declare the mapping. FULLY VERIFIED end to end 2026-08-31 against a live local Virtuoso 8.3.3335 instance, including a live SPARQL query against the resulting graph returning the correct rows. Two universal fixes are baked in: 'role' is a Virtuoso reserved word (CREATE TABLE role fails on any Virtuoso instance, not just this one) — renamed to org_role throughout; and the RDF View section uses ALTER QUAD STORAGE virtrdf:DefaultQuadStorage with all FROM-table aliases and all mapping predicates in ONE statement, since a second statement targeting the same GRAPH would replace rather than extend the first. One caveat found during verification: the reportsTo chain is fully present as single-hop triples, but SPARQL property-path traversal (reportsTo+) over this RDF-View-backed predicate returned zero rows on this instance — see :sparqlReportingChain. EXTENDED 2026-09-02 with a fourth step (ontology load) and re-verified end to end, including on a second, independent Virtuoso instance (demo.openlinksw.com) — see :rdfViewsMapping's schema:description for the full account.

-- ═══════════════════════════════════════════════════════════════════
-- acmebank.sql — AcmeBank operating-structure schema, sample data,
-- RDF View (Quad Map) declaration, and ontology (TBox) load, in one
-- executable batch.
-- Run with: isql localhost:1111 dba <password> < acmebank.sql
-- (or paste into Virtuoso Conductor > Interactive SQL)
-- FULLY VERIFIED end to end against a live Virtuoso 8.3.3335 instance
-- (2026-08-31, extended 2026-09-02), including a live SPARQL SELECT over
-- the resulting graph, on both a local instance and demo.openlinksw.com.
-- ═══════════════════════════════════════════════════════════════════

-- ── 1. Schema ─────────────────────────────────────────────────────────
CREATE TABLE organisation (
  org_id       INTEGER PRIMARY KEY,
  org_name     VARCHAR(120) NOT NULL,
  parent_org_id INTEGER REFERENCES organisation(org_id),
  valid_from   DATE,
  valid_to     DATE
);

CREATE TABLE business_function (
  function_id  INTEGER PRIMARY KEY,
  function_name VARCHAR(120) NOT NULL,
  org_id       INTEGER NOT NULL REFERENCES organisation(org_id)
);

CREATE TABLE org_role (
  role_id      INTEGER PRIMARY KEY,
  role_title   VARCHAR(120) NOT NULL,
  seniority    VARCHAR(40),
  function_id  INTEGER NOT NULL REFERENCES business_function(function_id),
  reports_to_role_id INTEGER REFERENCES org_role(role_id),
  scoped_from  DATE,
  scoped_to    DATE
);

CREATE TABLE person (
  person_id    INTEGER PRIMARY KEY,
  full_name    VARCHAR(160) NOT NULL,
  filler_kind  VARCHAR(20) DEFAULT 'Person'
    CHECK (filler_kind IN ('Person','Agent'))
);

CREATE TABLE role_assignment (
  assignment_id INTEGER PRIMARY KEY,
  role_id      INTEGER NOT NULL REFERENCES org_role(role_id),
  person_id    INTEGER NOT NULL REFERENCES person(person_id),
  filled_from  DATE NOT NULL,
  filled_to    DATE
);

-- ── 2. Sample data ────────────────────────────────────────────────────
INSERT INTO organisation (org_id, org_name, parent_org_id) VALUES
  (1, 'AcmeBank', NULL),
  (2, 'SMB Banking', 1);

INSERT INTO business_function (function_id, function_name, org_id) VALUES
  (1, 'SMB Lending', 2);

INSERT INTO org_role (role_id, role_title, seniority, function_id, reports_to_role_id) VALUES
  (1, 'CEO', 'Executive', 1, NULL),
  (2, 'Head of SMB Banking', 'Senior', 1, 1),
  (3, 'Head of SMB Lending', 'Senior', 1, 2),
  (4, 'SMB Loan Underwriter', 'Individual Contributor', 1, 3);

INSERT INTO person (person_id, full_name) VALUES
  (1, 'Eleanor Fairbairn'),
  (2, 'Yusuf Rahman');

INSERT INTO role_assignment (assignment_id, role_id, person_id, filled_from, filled_to) VALUES
  (1, 3, 1, '2023-01-01', '2026-01-01'),  -- Eleanor Fairbairn, Head of SMB Lending (past)
  (2, 3, 2, '2026-01-01', NULL);          -- Yusuf Rahman, Head of SMB Lending (current)

-- ── 3. RDF View (Quad Map) declaration — VERIFIED live 2026-08-31 ────────
SPARQL CREATE IRI CLASS <role_iri> "https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl#role/%d" (in role_id integer not null) ;
SPARQL CREATE IRI CLASS <person_iri> "https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl#person/%d" (in person_id integer not null) ;
SPARQL CREATE IRI CLASS <assignment_iri> "https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl#assignment/%d" (in assignment_id integer not null) ;

-- All mappings for one target graph must be in ONE ALTER QUAD STORAGE
-- statement -- a second statement targeting the same GRAPH replaces the
-- first rather than adding to it.
SPARQL
ALTER QUAD STORAGE virtrdf:DefaultQuadStorage
  FROM DB.DBA.org_role AS role_tbl
  FROM DB.DBA.org_role AS role_tbl_1
  FROM DB.DBA.role_assignment AS ra_tbl
  FROM DB.DBA.person AS p_tbl
{
  GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl> {
    -- role_tbl_1 is a SECOND alias of org_role, required for the
    -- reports_to_role_id self-referencing foreign key.
    <role_iri> (role_tbl.role_id)
      <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#OrgRole> as virtrdf:map_role_type ;
      <http://schema.org/name> role_tbl.role_title as virtrdf:map_role_name ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#reportsTo> <role_iri> (role_tbl_1.role_id)
        where ( ^{role_tbl.}^.reports_to_role_id = ^{role_tbl_1.}^.role_id ) as virtrdf:map_role_reportsTo .

    -- filledBy/filledFrom/filledTo are bound to assignment_iri (one per
    -- role_assignment row), never to role_iri directly -- see the
    -- cross-join note in :rdfViewsMapping's schema:description.
    <assignment_iri> (ra_tbl.assignment_id)
      <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#RoleAssignment> as virtrdf:map_assignment_type ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#forRole> <role_iri> (ra_tbl.role_id) as virtrdf:map_forRole ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#filledBy> <person_iri> (ra_tbl.person_id) as virtrdf:map_filledBy ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#filledFrom> ra_tbl.filled_from as virtrdf:map_filledFrom ;
      <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#filledTo> ra_tbl.filled_to as virtrdf:map_filledTo .

    <person_iri> (p_tbl.person_id)
      <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#OrgPerson> as virtrdf:map_person_type ;
      <http://schema.org/name> p_tbl.full_name as virtrdf:map_person_name .
  }
} ;

-- ── 4. Ontology (TBox) load — same annotated term definitions as
-- :orgOntology below, loaded into the SAME graph as the ABox above so a
-- DESCRIBE or type query returns them together. VERIFIED live on both
-- the local instance and demo.openlinksw.com 2026-09-02.
SPARQL
PREFIX post: <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#>
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX schema: <http://schema.org/>
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
INSERT DATA {
  GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl> {
    post:orgOntology a owl:Ontology ;
      rdfs:label "AcmeBank Org Ontology (RDF View target)"@en ;
      rdfs:comment "Lightweight OWL vocabulary the AcmeBank RDF View instantiates, loaded as TBox triples into the same named graph as the ABox instance data."@en .
    post:OrgRole a owl:Class ; rdfs:label "Organizational Role"@en ;
      rdfs:comment "A named position in AcmeBank's reporting hierarchy, instantiated once per acmebank_role row. Closer to org:Post than org:Role's general classification sense."@en ;
      rdfs:isDefinedBy post:orgOntology ; rdfs:seeAlso org:Post, org:Role .
    post:OrgPerson a owl:Class ; rdfs:label "Organizational Person"@en ;
      rdfs:comment "A human filler of an OrgRole, instantiated once per acmebank_person row."@en ;
      rdfs:isDefinedBy post:orgOntology ; owl:equivalentClass schema:Person .
    post:RoleAssignment a owl:Class ; rdfs:label "Role Assignment"@en ;
      rdfs:comment "Reifies one filled-role fact as its own resource, instantiated once per acmebank_role_assignment row."@en ;
      rdfs:isDefinedBy post:orgOntology ; owl:equivalentClass prov:Attribution ; rdfs:seeAlso schema:Role, org:Membership .
    post:reportsTo a owl:ObjectProperty, owl:AsymmetricProperty, owl:IrreflexiveProperty, owl:FunctionalProperty ;
      rdfs:label "reports to"@en ;
      rdfs:comment "Direct-line management edge, OrgRole to OrgRole. Deliberately NOT owl:TransitiveProperty -- and property-path/TRANSITIVE traversal over this RDF-View-backed predicate returns zero rows on this instance."@en ;
      rdfs:domain post:OrgRole ; rdfs:range post:OrgRole ; rdfs:isDefinedBy post:orgOntology ; rdfs:subPropertyOf org:reportsTo .
    post:forRole a owl:ObjectProperty, owl:AsymmetricProperty, owl:IrreflexiveProperty, owl:FunctionalProperty ;
      rdfs:label "for role"@en ; rdfs:comment "The OrgRole a RoleAssignment fills; exactly one per assignment."@en ;
      rdfs:domain post:RoleAssignment ; rdfs:range post:OrgRole ; rdfs:isDefinedBy post:orgOntology ; rdfs:seeAlso org:role .
    post:filledBy a owl:ObjectProperty, owl:AsymmetricProperty, owl:IrreflexiveProperty, owl:FunctionalProperty ;
      rdfs:label "filled by"@en ; rdfs:comment "The OrgPerson holding a RoleAssignment; exactly one per assignment."@en ;
      rdfs:domain post:RoleAssignment ; rdfs:range post:OrgPerson ; rdfs:isDefinedBy post:orgOntology ; rdfs:subPropertyOf org:member .
    post:filledFrom a owl:DatatypeProperty, owl:FunctionalProperty ;
      rdfs:label "filled from"@en ; rdfs:comment "The date a RoleAssignment began."@en ;
      rdfs:domain post:RoleAssignment ; rdfs:range xsd:date ; rdfs:isDefinedBy post:orgOntology ; rdfs:subPropertyOf schema:startDate .
    post:filledTo a owl:DatatypeProperty, owl:FunctionalProperty ;
      rdfs:label "filled to"@en ; rdfs:comment "The date a RoleAssignment ended; absent for the current holder."@en ;
      rdfs:domain post:RoleAssignment ; rdfs:range xsd:date ; rdfs:isDefinedBy post:orgOntology ; rdfs:subPropertyOf schema:endDate .
  }
} ;

LiveProof, Not Promise: A Live Virtuoso Server You Can Query Yourself

This is not a demo. It runs — on a server you can query yourself, right now.

Everything above — the SQL schema, the sample data, the Quad Map, the ontology — was built and proven first on an ordinary development machine, the way any engineer tests before shipping. That machine is not the point. The point is what happened next: the identical schema, the identical sample rows, the identical Quad Map, and the identical ontology were carried, unchanged, onto demo.openlinksw.com — a public Virtuoso 8.3.3335 server, reachable by anyone. Nothing was rebuilt for the occasion.

The vocabulary — reportsTo, forRole, filledBy, filledFrom, filledTo, OrgRole, OrgPerson, RoleAssignment — travels with the design, not with the machine it happens to sit on. Move the schema, and the meaning moves with it.

Here is the proof, and you do not have to take our word for it: every OrgRole and OrgPerson named in this document carries a direct link to the live resource serving from demo.openlinksw.com. Click one. Your browser lands on a real page, assembled the instant you asked for it, by a query compiled back to ordinary SQL against ordinary tables — not a document pulled from a cache.

Hand this same schema and this same Quad Map to any DBA already running Virtuoso, and by the end of the day they will have a production Linked Data graph — dereferenceable, typed, standards-compliant — running on infrastructure they already own. No import job. No second copy of the data quietly drifting out of sync. Just the database they already have, doing one more thing well.

Live demo.openlinksw.com

The public Virtuoso server this claim is actually about. If a link below shows an “Open this link?” page first, that is a real, site-wide click-through confirmation on demo.openlinksw.com — not a broken link. Click Continue to reach the live description page.

Built and proven first on an ordinary development testbed, before being carried to the public server above — CEO Head of SMB Banking Head of SMB Lending SMB Loan Underwriter

Six SPARQL recipes

SPARQL

Who currently owns SMB Lending?

Run live query

SPARQL equivalent of the article's first worked query, runnable live against this document's own companion graph via the footer SPARQL Workbench — returns exactly one row, Yusuf Rahman, because his role_assignment row has no filledTo end date. filledBy/filledTo are scoped to a per-assignment resource rather than the shared role, so the OPTIONAL/FILTER pairing cannot cross-join with the prior holder's end date. VERIFIED 2026-08-31: run live against the :sqlBatchScript RDF View on a real Virtuoso 8.3.3335 instance, this exact query pattern returned exactly the expected single row. VERIFIED AGAIN 2026-09-02 against the actual public endpoint this section’s Run live query button targets (linkeddata.uriburner.com/sparql), after this document’s own TTL was published there — same single expected row.

PREFIX post: <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#>
PREFIX schema: <http://schema.org/>
SELECT ?roleIri ?person ?personIri WHERE {
  GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl> {
    ?roleIri schema:name "Head of SMB Lending"@en .
    ?assignment post:forRole ?roleIri ;
                post:filledBy ?personIri .
    OPTIONAL { ?assignment post:filledTo ?filledTo }
    FILTER (!BOUND(?filledTo))
    ?personIri schema:name ?person .
  }
}
SPARQL

Who has ever held Head of SMB Lending?

Run live query

SPARQL equivalent of the article's second worked query, runnable live against this document's own companion graph via the footer SPARQL Workbench — returns both Eleanor Fairbairn (2023-2026, inactive) and Yusuf Rahman (2026-present, active), ordered by fill start date. VERIFIED 2026-08-31: run live against the :sqlBatchScript RDF View on a real Virtuoso 8.3.3335 instance, this exact query pattern returned exactly the expected two rows in the expected order. VERIFIED AGAIN 2026-09-02 against the actual public endpoint this section’s Run live query button targets (linkeddata.uriburner.com/sparql), after this document’s own TTL was published there — same two expected rows, same order.

PREFIX post: <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#>
PREFIX schema: <http://schema.org/>
SELECT ?person ?personIri ?filledFrom ?filledTo WHERE {
  GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl> {
    ?roleIri schema:name "Head of SMB Lending"@en .
    ?assignment post:forRole ?roleIri ;
                post:filledBy ?personIri ;
                post:filledFrom ?filledFrom .
    OPTIONAL { ?assignment post:filledTo ?filledTo }
    ?personIri schema:name ?person .
  }
} ORDER BY ?filledFrom
SPARQL

Reporting chain from SMB Loan Underwriter to CEO

Run live query

SPARQL equivalent of the article's third worked query. A single property-path expression (post:reportsTo+) walks the whole chain in one traversal, the SPARQL analogue of Cypher's variable-length REPORTS_TO* pattern — and works normally if this graph is populated the ordinary way, by loading this document's own Turtle file. CAVEAT found during 2026-08-31 live verification: when this graph is instead populated via the :sqlBatchScript RDF View (SQL tables + Quad Map, no Turtle upload — the scenario this section of the document actually demonstrates), every individual reportsTo hop is present and directly queryable, but the reportsTo+ property path itself returned zero rows over that RDF-View-backed predicate on the verified Virtuoso 8.3.3335 instance — as did Virtuoso's native OPTION (TRANSITIVE, ...) extension. Recursive traversal over a virtual (SQL-compiled) predicate does not behave like traversal over materialized triples here. Walk the chain with repeated single-hop queries instead when the graph is RDF-View-backed. VERIFIED 2026-09-02: after this document’s own TTL was published to linkeddata.uriburner.com (ordinary Turtle load, not RDF-View-backed), this exact reportsTo+ property-path query, run against the actual public endpoint this section’s Run live query button targets, returned exactly the expected three-hop chain — SMB Loan Underwriter to Head of SMB Lending to Head of SMB Banking to CEO.

PREFIX post: <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.html#>
PREFIX schema: <http://schema.org/>
SELECT ?startRoleIri ?startRole ?ancestorRoleIri ?ancestorRole WHERE {
  GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl> {
    ?startRoleIri schema:name "SMB Loan Underwriter"@en ; schema:name ?startRole .
    ?startRoleIri post:reportsTo+ ?ancestorRoleIri .
    ?ancestorRoleIri schema:name ?ancestorRole .
  }
}
SPARQL

Federating the org RDF View with DBpedia

Run live query

SPARQL-FED joins this document's own companion graph against an external endpoint in one query — no import step, something a single Neo4j instance cannot do natively. VERIFIED WORKING 2026-09-02, after two real bugs were found and fixed by actually running it against the live public endpoint: (1) DBpedia's current dataset no longer carries dbo:abstract for dbr:Neo4j — the live, current predicate is dbo:description, verified directly against dbpedia.org/sparql before use (value: “graph database implemented in Java”@en). (2) This Virtuoso instance's SPARQL engine throws “Error SR549: Both datatype id and language id are not default in call of rdf_box()” whenever a language-tagged literal crosses a SERVICE federation boundary and is re-materialized locally — reproducible with ANY language-tagged value from a remote SERVICE call. WORKAROUND: wrap the value in STR() inside the SERVICE block's own SELECT, stripping the language tag before it crosses back — the join and role name still bind correctly.

PREFIX schema: <http://schema.org/>
PREFIX dbo: <http://dbpedia.org/ontology/>
SELECT ?role ?neo4jAbstract WHERE {
  GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl> {
    ?roleIri schema:name "Head of SMB Lending"@en ; schema:name ?role .
  }
  SERVICE <https://dbpedia.org/sparql> {
    SELECT (STR(?abstractRaw) AS ?neo4jAbstract) WHERE {
      <http://dbpedia.org/resource/Neo4j> dbo:description ?abstractRaw .
      FILTER (lang(?abstractRaw) = "en")
    }
  }
}
SPARQL

Context-selection query — agent-rdf-memory's own operating structure

Run live query

Representative of the Mandatory Retrieval Sequence. Scoped to a local-only named graph (urn:dav:/DAV/home/kidehen/rdf-import-test/preferences.ttl) that exists only on the operator's own Virtuoso instance — the live-query link will return zero rows or a graph-not-found result against the public UB endpoint, unlike the recipes above that are scoped to this document's own companion graph.

PREFIX schema: <http://schema.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?step ?stepIri ?howtoDoc WHERE {
  GRAPH <urn:dav:/DAV/home/kidehen/rdf-import-test/preferences.ttl> {
    ?stepIri a schema:HowToStep ;
             schema:name ?step ;
             rdfs:seeAlso ?howtoDoc .
    FILTER(CONTAINS(LCASE(?step), "sparql"))
  }
}
SPARQL

Entity-type summary of this document's own knowledge graph

Run live query

A SAMPLE-based summary over this document's own companion graph — one representative instance IRI per rdf:type.

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?type (SAMPLE(?s) AS ?exampleIri) (COUNT(?s) AS ?count) WHERE {
  GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-ekl-operating-structure-vs-virtuoso-claude_sonnet_5-1.ttl> {
    ?s rdf:type ?type .
  }
} GROUP BY ?type ORDER BY DESC(?count)
Agent Memory · Live Deployment

The Same Pattern, Governing the Agent Itself

agent-rdf-memory already runs the Operating Structure chapter's core idea — a queryable, machine-readable substrate an agent consults before acting, instead of re-deriving context from stale prompts — against a local Virtuoso instance today. Where AcmeBank's graph answers “who owns SMB Lending”, this deployment answers “which behavioral rule governs this task, and which howto document must I read before acting on it.”

📡

SPARQL endpoint

Local Virtuoso instance at https://localhost/sparql, queried at the start of every session.

📂

WebDAV mirror

Every agent-rdf-memory/{relpath} file is PUT to a matching DAV path, exposed as named graph urn:dav:/DAV/home/kidehen/rdf-import-test/{relpath} — a 1:1 mirror of the filesystem tree.

📋

Mandatory Retrieval Sequence

A 9-step session-start protocol — the agent's own REPORTS_TO chain, walked as rdfs:seeAlso links between preference steps and howto documents.

Does the platform reuse its own pattern?

Neo4j's roadmap defers agent tooling and memory to chapters 6–7 of the same series, described as future work layered on top of the operating structure. Virtuoso's agent-rdf-memory already runs the identical pattern today, governing the very agent that generated this comparison.

HowTo

Rebuilding Chapter 1 on Virtuoso, Then Pointing It at the Agent Itself

1

Model the five entity types as SQL tables

Create organisation, business_function, role, person, and role_assignment tables, putting validity-window columns only where the article puts validity dates — PART_OF-equivalent, SCOPED_TO-equivalent, and FILLED_BY-equivalent.

2

Load sample data reproducing AcmeBank's reporting chain

Insert the CEO / Head of SMB Banking / Head of SMB Lending / SMB Loan Underwriter chain, plus Eleanor Fairbairn's 2023–2026 tenure and Yusuf Rahman's 2026–present tenure in Head of SMB Lending.

3

Declare RDF Views mapping the tables to the org ontology

Write Quad Map Patterns binding each role/person row to a dereferenceable IRI and each foreign key to an org:reportsTo or org:filledBy triple — a zero-copy virtual graph.

4

Run the three Neo4j worked queries as SPARQL

Current accountability becomes a FILTER for an unbound filledTo; historical holders becomes an ORDER BY; the reporting-chain traversal becomes a single org:reportsTo+ property path.

5

Type the FILLED_BY-equivalent edge with PROV-O

Add prov:Person / prov:SoftwareAgent typing to each role filler and prov:actedOnBehalfOf where an agent acts under a human principal's authority.

6

Federate the org graph with an external SPARQL endpoint

Use SPARQL-FED's SERVICE clause to join the local RDF View against a remote endpoint — demonstrated here against DBpedia — in one query.

7

Point the identical pattern at agent-rdf-memory itself

Mirror agent-rdf-memory/{relpath} into a Virtuoso named graph via WebDAV PUT, then run the Mandatory Retrieval Sequence's context-selection SPARQL query — now governing the agent that built this comparison.

FAQ

Frequently Asked Questions

That AI agents cannot operate reliably inside a business without a queryable, machine-readable answer to “who owns what” — an answer that today typically lives in people's heads, stale slides, or hidden inside complex systems, per the article's Rosa Delgado example.

Organisation, BusinessOrganisation, BusinessFunction, Role, and Person — joined by PART_OF, HAS_FUNCTION, SCOPED_TO, REPORTS_TO, and FILLED_BY relationships.

PART_OF, SCOPED_TO, and FILLED_BY — because organisational restructuring, role scoping changes, and personnel changes actually happen on those edges. HAS_FUNCTION and REPORTS_TO are treated as current facts only, by deliberate convention.

It maps the existing SQL tables to RDF using Virtuoso RDF Views (Quad Map Patterns, the native equivalent of an R2RML mapping) — a zero-copy virtual graph. SPARQL queries compile back to SQL at query time against the live rows.

organisation, business_function, role, person, and role_assignment — the last of which carries the filled_from/filled_to validity window that plays FILLED_BY's role.

A SELECT filters role_assignment rows for Head of SMB Lending where no filledTo end date is bound — returning Yusuf Rahman, matching Neo4j's single-row result exactly.

With a SPARQL property path, org:reportsTo+, the direct analogue of Cypher's variable-length REPORTS_TO* pattern — it walks from SMB Loan Underwriter through Head of SMB Lending and Head of SMB Banking to CEO in one traversal.

Federate the live org RDF View against an independent SPARQL endpoint in one query via SPARQL-FED's SERVICE clause — demonstrated here joining AcmeBank role data with DBpedia's description of Neo4j itself.

By typing the role filler explicitly with PROV-O (prov:Person vs prov:SoftwareAgent) and recording prov:actedOnBehalfOf where an agent acts under a human's authority.

It is a live deployment: agent-rdf-memory's Turtle files are mirrored into named graphs on a local Virtuoso instance (SPARQL endpoint https://localhost/sparql), synced via WebDAV PUT immediately after any edit, and queried by the agent's own 9-step Mandatory Retrieval Sequence at the start of every session.

Both are queryable structures an actor consults before acting instead of trusting whatever happens to already be in its context window. AcmeBank's graph answers “who owns SMB Lending, and who do they report to”; agent-rdf-memory's graph answers “which behavioral rule governs this task, and which howto document must I read before acting on it.”

The article defers agent tooling and memory loops to chapters 6 and 7 of the same series, describing agents that will “query accountability context before acting and write execution traces back into the graph” — future work for a pattern Virtuoso's agent-rdf-memory already runs today.

It's live, not just described here: after being built and proven on a development instance, the identical schema, sample data, Quad Map, and ontology were carried unchanged onto demo.openlinksw.com, a public Virtuoso server anyone can query. Every OrgRole and OrgPerson entity in this document carries an owl:sameAs to the matching resource actually serving from that public deployment — see the proof section above.

Glossary

Terms

Operating Structure

Chapter 1 of Neo4j's Enterprise Knowledge Layer series: a graph model of who owns what inside an organisation, meant as the foundation later chapters attach to.

Property graph

Neo4j's data model: labeled nodes and relationships, each carrying key-value properties — the model AcmeBank's Organisation/Role/Person structure is built in.

RDF Views

Virtuoso's mechanism for exposing relational tables as a virtual RDF graph without copying data — SPARQL against an RDF View is compiled to SQL and run live.

R2RML

The W3C standard for mapping relational databases to RDF; Virtuoso's native Quad Map Patterns predate it but express the same declarative mapping idea.

Cypher

Neo4j's graph query language; the article's worked queries and schema/data-load scripts are all written in Cypher 25.

SPARQL-FED (federated SPARQL)

SPARQL's SERVICE clause, which lets one query join data held at any number of independent SPARQL endpoints.

Zero-copy (virtual) graph

A graph exposed over existing data in place, with no ETL step and no duplicate copy to keep in sync.

PROV-O

The W3C provenance ontology, used here to type role fillers explicitly and record which agent acted on whose authority.

Dereferenceable IRI

An identifier that is also a working HTTP URL — every Role, Person, and BusinessFunction in the Virtuoso alternative is one.

Named graph

A labeled subset of a quad store's triples, addressable by its own IRI — the mechanism agent-rdf-memory's Virtuoso deployment uses to mirror each filesystem file.

Graph type

Neo4j's Enterprise-Edition-only schema enforcement mechanism, naming allowed node types, properties, and relationships.

Quad Map Pattern

Virtuoso's native syntax for declaring an RDF View, in production since roughly 2007 — functionally what R2RML later standardized.

Knowledge Graph Explorer 192 nodes · 384 links

Interactive graph visualization derived from the companion RDF. Click nodes to resolve, drag to explore. Graph data embedded from companion RDF at generation time.

Operating Structure — Neo4j Model, Virtuoso Rebuild, Agent Memory

Nodes: 0 Links: 0
Click SVG to activate zoom, click outside to release | Drag nodes to pin, double-click to unpin
Classes Properties Instances

Explore Knowledge Graph using SPARQL

Choose a query recipe, edit the SPARQL if needed, then open the encoded URIBurner query.

Run live query

SELECT uses text/x-html+tr. DESCRIBE and CONSTRUCT use text/x-html-nice-turtle, matching the SPARQL format guidance in the skill contract.