A dedicated property graph — labeled nodes and relationships with properties.
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.
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.
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.
The enterprise entity itself — AcmeBank — at the root of the graph.
A named activity such as SMB Lending, anchoring later layers of the series.
A position with a title and seniority, scoped to a function, reporting to another role.
Fills a Role via FILLED_BY — an edge the article notes silently also accepts an Agent node.
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.
Each row below is its own described entity in the companion RDF — click a dimension name to look it up.
| Dimension | Neo4j (Chapter 1) | Virtuoso alternative |
|---|---|---|
| Data model | A 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 language | Cypher 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 declaration | An 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 exists | HR/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 identity | Internal 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 federation | Queries 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 filler | FILLED_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 relationships | Validity 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 provenance | Entity 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 longevity | This 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. |
A 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.
Cypher 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.
An 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.
HR/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.
Internal 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.
Queries 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.
FILLED_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.
Validity 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.
Entity 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.
This 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.
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.
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.
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'))
);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
);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)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 .
}
} ;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 .
}
} ;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.
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
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 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 ?filledFromSPARQL 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-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")
}
}
}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"))
}
}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-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.”
Local Virtuoso instance at https://localhost/sparql, queried at the start of every session.
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.
A 9-step session-start protocol — the agent's own REPORTS_TO chain, walked as rdfs:seeAlso links between preference steps and howto documents.
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.
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.
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.
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.
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.
Add prov:Person / prov:SoftwareAgent typing to each role filler and prov:actedOnBehalfOf where an agent acts under a human principal's authority.
Use SPARQL-FED's SERVICE clause to join the local RDF View against a remote endpoint — demonstrated here against DBpedia — in one query.
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.
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.
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.
Neo4j's data model: labeled nodes and relationships, each carrying key-value properties — the model AcmeBank's Organisation/Role/Person structure is built in.
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.
The W3C standard for mapping relational databases to RDF; Virtuoso's native Quad Map Patterns predate it but express the same declarative mapping idea.
Neo4j's graph query language; the article's worked queries and schema/data-load scripts are all written in Cypher 25.
SPARQL's SERVICE clause, which lets one query join data held at any number of independent SPARQL endpoints.
A graph exposed over existing data in place, with no ETL step and no duplicate copy to keep in sync.
The W3C provenance ontology, used here to type role fillers explicitly and record which agent acted on whose authority.
An identifier that is also a working HTTP URL — every Role, Person, and BusinessFunction in the Virtuoso alternative is one.
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.
Neo4j's Enterprise-Edition-only schema enforcement mechanism, naming allowed node types, properties, and relationships.
Virtuoso's native syntax for declaring an RDF View, in production since roughly 2007 — functionally what R2RML later standardized.
Interactive graph visualization derived from the companion RDF. Click nodes to resolve, drag to explore. Graph data embedded from companion RDF at generation time.
Choose a query recipe, edit the SPARQL if needed, then open the encoded URIBurner 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.