
Graphs usually get a bad rap: “what are they good for?”, “there’s no schema”. But there are also moments when they are superior to tables and other formats, and agents like them a lot. Typed graphs, meaning graphs with clear data types attached, help even more, especially with types that can be checked and verified. If you can define it declaratively, that helps collaboration by giving humans and agents a place to work together, but also data quality, as you can version it and extensively run tests (unit, end-to-end, etc.).
Data quality was probably the biggest and most challenging domain in data two decades ago, and it is getting even harder these days with more autonomous systems and agents. The question becomes: how do we build and verify reliably? That’s where graphs as code with added types and checks can ensure that agents won’t store wrong data in the wrong place.
But how do we test? In the old days, we got a confirmed business number from the business users or domain experts. They consolidated it either from their CRM, SAP, or other operational applications they use (sometimes from their Excel sheets 😉). Or, the second most common case, people created queries based on an OLTP relational database, which give numbers verified by the business that then need to be replicated by the DWH or analytics team. So, as a start, we could use that and feed it to the agents1.
No matter how we test, types and checks are always part of it, and that’s why they’re making their way into graphs, enabling more agentic work with graph as code, and overall consistency.
Why Is Agentic Engineering Built on Graphs?
So let’s first start with why agentic engineering uses graphs, why they are therefore popular nowadays, and why context and knowledge layers build on top of them, too.
Three Shapes of Data: tabular data -> JSON -> Graphs
If we zoom out, we have essentially three shapes of data2:
- Good old tabular data: everything that looks like a table with rows and columns (Excel style, databases)—these are rigid and typed, with relationships modeled in foreign keys and an Entity Relationship Diagram (ERD)
- Then there’s the nested tree structure like JSON/XML, etc.: useful for semi-structured data, popularized by the NoSQL movement. Great for storing, not so much for analytics—flexible, untyped, and hard at query time.
- And graphs with nodes (also called vertices) and edges (also called links or lines) modeled with relationships between them—relationships are first-class, but untyped.
links_to. In this example, nothing says why two notes connect. Relationships first-class, but untyped.So why did LLMs and AI architecture not choose a relational, and therefore tabular, format, or a simple JSON?
Why a Graph over a Relational Model (SQL)?
The simple reason isn’t storage or joins, but retrieval. Graphs treat relationships as first-class data, so retrieval matches the shape of the questions.
When an agent works with messy, heterogeneous knowledge (people, docs, concepts, decisions, code), its questions are usually relational and open-ended. “What’s connected to X?” “How did A lead to B?” “What else touches this concept?” A graph answers these by walking edges from a starting point. SQL answers them by reconstructing relationships through joins at query time.
The advantages of a graph are many. Multi-hop traversals are much cheaper than including 100s of tables in a SQL join with CTEs. The schema of a “table” can stay loose and doesn’t have a fixed table design by default. Edges can carry the meaning of why two nodes are connected, which is helpful for retrieval again. A classical foreign key does not have that built in. There, we usually add it to the Entity Relationship Diagram visually, but not in the database or the relationship itself.
Retrieval with a graph becomes finding an entry point with hybrid search (vector + BM25), then expanding its neighborhood into a subgraph the model can read as context.
SQL still wins for aggregation and well-structured records, and also for type safety and overall reliability and consistency, which get ever more important with agents to keep data quality up.
With agents being the user, workflow, and operator at once, having schema, queries, and policy as one checkable definition in code becomes more important than ever. That’s where typed graphs and graph as code with checked types bridge the gap.
Signs of SQL Not Being Enough
SQL is great for its declarative approach: you can write SQL independently of the engine, and the engine will figure out the best execution (query plan) and how to retrieve and return tabular data.
But there are also limits. Julian Hyde gave a talk at Data Council about Extending SQL for analytics. He continued his work with Morel Language with static types, unit tests, and more:
Morel can solve data-engineering and data-intensive problems that are beyond SQL’s capabilities (think dbt and Spark) but with the benefit of a single compiler/optimizer. Even problems that can be fully solved in SQL will, I think, benefit from the programming language approach (static typing, unit tests, refactoring, modules, version control).
Also, Andy Pavlo said on the MAD Podcast that he has strong opinions about graph databases (not graphs per se):
[…] the traversals of graphs, which essentially are just self-joins on a table. You can implement those very efficiently, and you can easily outperform Neo4j. […] If you’re doing that back and forth [client-side traversal], yeah, they’ll beat you. But the SQL standard now supports property graph queries. […] So graph databases, I think a horrible idea. There’s no reason why anyone would actually want to use them.
This was said in the context of graph engines for OLTP. He also says that plain SQL is the wrong language for traversal, which he wrote in What Goes Around Comes Around… And Around… (SIGMOD Record, 2024) as well:
“RDBMSs are always an option to support graphs. But ‘vanilla’ SQL is not expressive enough for graph queries and thus require multiple client-server roundtrips for traversal operations.”
Relations with Unstructured Knowledge
In Omnigraph, we don’t build relational data. We are building connections with unstructured knowledge. That’s why a graph with its language on top, and columnar tables stored underneath in a file structure3, is very useful for the knowledge use case.
So a ‘graph-based storage format’, not a database, helps with context. The graph wins as the model, schema, and interface for agents, and not as the storage engine.
Another case is plain old Postgres, which almost shipped GRAPH_TABLE (SQL/PGQ) in version 19: committed for the beta, then reverted in September 2026 over readiness concerns, so 19 ships without it and it is now PG20 material. But even once it lands, Postgres is a general-purpose database. It’s great if you need to store some graph-like data, but not if the full knowledge graph and agents depend on graphs.
The prediction by Gartner in 2025 that:
“By 2026, graph technologies will be used in 80% of data and analytics innovations, up from 10% in 2021, facilitating rapid decision making across the enterprise.” Even from other attendees, we heard “being built on a knowledge graph early will result in the best catalog approach today. This growth in the awareness and adoption of graph technology is now being seen as a key step to realizing future AI success. We’re excited to see what that brings.”
It’s hard to say if that came true, but with the focus on graphs, context layers, ontologies, and semantics, it’s clear that more people are implementing or looking at graphs, but only time will tell.
But all of this goes to show that SQL, which I love, use all the time, and will continue to use, also has its limitations, or use cases where other approaches are better. With LLMs’ more complex and unpredictable needs and non-linear traversal across millions of interconnected data points, processing them in main memory using specialized data structures is essential. That’s where (analytic) graph applications make more sense than traditional storage systems.
Graph ≠ Graph database ≠ BI chart ≠ NoSQL
In this article, we talk a lot about graphs and graph databases, but they’re not the same thing. A graph is a data model: things (nodes) and the relationships between them (edges), where each relationship has a name, a direction, and optionally its own properties. It’s a way to describe data, and you can store it anywhere: Markdown files with wikilinks, JSON, Parquet or Lance files, or a plain node table plus an edge table in Postgres.
A graph database is an engine built to store and traverse that model natively, like Neo4j with its own storage layout, its own query language (Cypher), and pointer-chasing execution.
An E-Graph (equality graph) is a data structure that holds many equivalent versions of an expression at once, so an optimizer can pick the best one. Cube, for example, uses one in its Rust-based semantic SQL engine to rewrite incoming queries into something its semantic layer can answer. Same nodes and edges, completely different job. So when someone says “graph”, it’s worth asking: a data model, a data structure, or a database?
How to Make Graphs Even More Useful for Agents: Typed Graph & Checked Type
Now that we know graphs are good for retrieval and for defining relationships as first-class citizens of the data, there are also some downsides that usually bite data quality. There’s not much consistency or structure. Or, as Fritz Laux, who researched the Typed Graph Model (TGM, more about it later), puts it: “the flexibility of the graph model contributes to its popularity, but its schema-less implementations are prone to data quality problems.” But that’s where typed graphs and checked types come into play.
What Are Typed Graphs?
So what’s a typed graph then? It’s a term I first heard with Omnigraph when I was building my company brain for large enterprises, though the term itself goes back to Laux’s 2020 paper (more below). In simple terms, it means adding data types to a graph, similar to data types in relational databases, increasing consistency and improving the quality of data written by agents. ‘Typed’ means rules: every node and edge has a declaratively defined shape, written down.
In a normal graph, relationships are first-class, but schemaless. Think of the Markdown wiki problem at scale. Laux makes the same point with Neo4j’s own “specification by example”: a user Billy with a 5-star review of a performance. From that one example, you can’t tell if Billy may review the same performance twice, or if a review can exist without a user or a performance. This is the connection to today and how we can’t force agents to write in a certain way.
Tabular:
| user_id | perf_id | stars |
|---|---|---|
| 1 | 7 | 5 |
JSON:
|
|
Graph:
Computer scientist Fritz Laux proves in his research on The Typed Graph Model (TGM) (2021) that the TGM is strictly stronger than the relational model (and at least as expressive as object-oriented, XML, and RDF schemas, which is why he calls it a supermodel), and he proposes:
a schema bound Typed Graph Model with properties and labels. These enhancements improve not only data quality but also the quality of graph analysis.
In his example, he makes the graph typed in his own definition with the schema: every node and edge type declares its properties with data types, which node types an edge may connect, a min-max cardinality on each end, and integrity constraints. Every node and edge in the graph maps to exactly one type, and “data type and constraint checking is applied for all nodes and edges before any insert, update, or delete action can be committed.”
The same Billy example as Omnigraph schema.pg, the typed version of the untyped brain graph above:
|
|
Four questions the Neo4j example left open are now answered in twelve lines: a review needs a User and a Performance (endpoints), stars is an integer (type)4, Billy can review Hamlet once (@unique), and a user may have zero or many reviews (@card). An agent writing stars: "five" or a second review is refused before the write lands.
That’s the write side, which Omnigraph often handles autonomously with agents, as we saw in Part 1. Laux even left the query side explicitly open: “the development of a manipulation and query language for the TGM is future work”. That gap is what checked types fill.
After NoSQL, MongoDB, and other “schema-free” approaches, DuckDB also introduced Typed Columns for JSON, adding types to JSON. The detail: json_transform turns a value it can’t convert into NULL by default, and only json_transform_strict raises an error. The default swallows the bad value, while the strict version surfaces it. Working with agents without schemas and enforced types gets messy very quickly, and a typed graph is the strict version on every write.
Omnigraph defines types as:
“typed property graph” and “typed ontology”, “a typed, versioned graph where your business entities, relationships, and constraints are defined as precisely as code. The schema compiler validates definitions before data enters the graph”.
What About Checked Types?
Typed graphs are important for the write side: the schema decides what an agent may store. Checked types are for the query side: what an agent may ask. For example, SQL itself checks a query only at prepare time against a live catalog5: you need a running database, and a bad query fails at runtime, often deep in an agent loop. A graph like Omnigraph can compile first: .gq → parse → typecheck → IR, omnigraph lint --query q.gq --schema schema.pg, stable error codes (Q parser, T type checker, P planner) with position and one concrete fix, offline, no running DB. A traversal over an edge that doesn’t exist or a filter on the wrong type is refused before anything executes.
|
|
Same diagnostic over the HTTP API, so an agent can parse it instead of reading prose:
|
|
It’s like a linter for agents. It speeds up query checks and quality, and the error is the documentation the agent acts on. A stored graph query becomes a typed and reusable tool: context as code. Define the traversal once, type-check it against the schema, version it with it, and the next agent reuses it instead of working it out again.
It has one plan and can traverse first, then run nearest only over reached nodes, including rrf(nearest, bm25). In the Shakespeare vs. Lion King example, the walk keeps only Shakespeare’s 37 plays, then ranks them by “a prince avenges his father’s murder”: Hamlet. Vector search alone over the whole graph puts The Lion King right next to it, same plot, wrong author. No app-side join between graph and vector store needed.
This converges with others, such as the previously mentioned Morel (static types, unit tests), Jev with typed outputs, or Lobu, where every agent output goes back into the same graph as an event or entity change. Different layers, same move: check output against a type instead of trusting it.
You might remember GraphQL, a query language to query such graphs that was quite popular in the Web 2.0 era when Facebook went fully mobile, so desktop and mobile had the same API to pull from. See also Building an Analytics API with GraphQL to get a feel for 2022, when GraphQL brushed data engineering. It looks like a typed graph, and it does validate every query against its schema before execution, with the SDL (Schema Definition Language) file usually in git. But that schema types the API response shape over whatever store sits behind it. A mutation is a typed function signature. What the resolver writes, and whether the stored relationships still match the SDL, nobody checks.
What a Typed Graph Allows: First-Class Agentic Engineering
It is essentially a declarative way of expressing knowledge with Graph as Code.
Compared to vector-only Retrieval-Augmented Generation (RAG), the difference isn’t the search: Omnigraph has the same nearest, bm25, and rrf as any vector store. The difference is that similar is not related. The top-k neighbors are close in embedding space, but nothing says how they relate to the question or to each other, so the agent has to work that out again on every call. In a typed graph, the edge type names the relationship, and the query that walks it is checked, versioned code: declarative retrieval instead of ad-hoc. Graph as code puts schema and queries in one definition that fits in context; assembling context is a traversal problem.
With ever-growing data, we need a more rigid way to work with ‘context’ to read, reuse, and change. Declaring schema, checked queries, and policies, all checked in and versioned in git and branches: this is what Omnigraph with a typed graph can deliver.
The Knowledge-Coordination Manifesto by Omnigraph says: “the schema is the price system for agents. It compresses domain knowledge into a structure every agent can read, trust, and act on. When the ontology is clear, agents make locally correct decisions. When it’s unclear, they produce chaos”.
Coordination through shared typed state avoids the need for an orchestrator, with just declarative definitions of business knowledge (context).
Coming from the Semantic Web angle:
- Resource Description Framework (RDF) Schema / OWL / SHACL: typed and constrained graphs since the 2000s.
- ISO GQL (ISO/IEC 39075:2024): includes graph types.
- PG-Schema (Angles et al., SIGMOD 2023), a schema language for property graphs.
- Laux’s Typed Graph Model (see above).
But what is new: the schema as a code file in git, queries checked offline against it, more stable machine-readable errors for agents, and branch-per-agent writes into one system. You could say, typed graphs finally get the developer workflow typed code already has.
So How Can the Typed Graph Help with Data Quality?
Coming back to the initial question of how typed graphs and checked types help data quality. Types are the cheapest and safest data quality check there is: they can’t be forgotten, they can’t be skipped, and they keep up with the writing speed of agents because every write is checked the moment it happens. Nobody has to review each write anymore. The type checker is the reviewer.
But there are two kinds of quality, and it helps to keep them apart. Data can be valid: right shape, right type, right place, right relationship. Types and checked queries solve this completely. And data can be true: the confirmed business number, the golden dataset, the end-to-end test from the intro. Types can’t help here. No schema knows whether revenue was really 4.2 million.
That’s what branches are for. An agent writes on its own branch, the result is diffed, reviewed, and merged into main or thrown away. The merge itself is type-checked again (schema validation runs on mutations, loads and merges), so a branch can’t smuggle invalid data into main either. Policies as code close the loop: protected_branches: [main] and Cedar on every write decide who may change what, in the same files as the schema.
Types answer valid at every arrow on the left. Only the human in the middle answers true.
The ModernRelay manifesto puts the same split in two words: humans define (what matters, what the schema is, what the rules are), agents execute (gather, structure, update, enrich). Types guard the executing side. Humans keep the defining side, and that’s where “true” lives.
It’s schema-on-write for knowledge, after a decade of schema-on-read data lakes. And yes, branching alone isn’t new (Neon and Dolt do it for SQL), but the combination of schema, queries, and policy living next to each other, as code, is a welcome improvement.
Graph as Code: Context as Code
All of it concludes with: schema (.pg), queries (.gq), policies (.yaml): three files in git, one diff, reviewed like any other pull request. The same reason code-first BI still beats Claude Dashboards applies here: you own the artifacts, you can version them, test them, and collaborate on them, human and agent alike. The team at ModernRelay counts six things that go declarative (schema, context, writes, policy, UI, deployment). This article covered the first four. The other two come out of the box: the UI is derived from schema and queries, and the whole graph is recreated anywhere with one apply.
So, how do we build and verify reliably, now that agents write faster than we can read? Build on a typed graph, and everything written is valid. Query with checked types, and every question is valid too. This allows agents to work on branches, so mistakes are cheap, and lets humans or domain experts chime in when changes are needed, because in the end, someone still has to decide what’s true. That part hasn’t changed since the days we called the business for the confirmed number, but it’s now a lot easier to get there.
If you want to catch up with the latest features, Omnigraph shipped a few in v0.14.0 and the edge builds since that fit this article directly: branch review and merge dry-runs to inspect what an agent changed before it lands, unpublished commits that stage a mutation until a human publishes or drops it, @card cardinality enforced on every write, an engine catalogue you query with the same typed GQ, an exportable agent skill built into the binary, and MCP tools listed only under the policy an agent holds.
-
But what if none exists, or if there’s not enough time to ask anyone and we just need to deliver? We could run it once in test with production data, or even run it in production without surfacing it yet, and use that going forward. You can also sample a larger test dataset whose outcome you know. But yeah, these are hard to create and usually don’t contain all edge cases. Still, it’s a great way for CI/CD, fast iteration, and autonomous agent TDD. ↩︎
-
For illustration, we use only these three. It’s a simplified view: nested trees are more than JSON and XML, and there are other shapes too, such as arrays/tensors, time series and event streams, unstructured data, key-value, and geospatial. ↩︎
-
e.g., columnar Lance tables with git branches, Arrow, and DataFusion, plus a dedicated in-memory CSR/CSC index per edge type ↩︎
-
Laux’s model also bounds it to 1..5. Omnigraph has that check as well:
@range(age, 0..150)on a node property is one line, enforced on every write.@rangeand@checkare node constraints,@cardis an edge constraint, and@key,@unique, and@indexwork on both. For an edge property likestars, the type check does the work in this example. ↩︎ -
Static checks for SQL exist, but as tools around the language rather than part of it, and most stop at syntax. Parsers like sqlparser-rs, pg_query, or JSQLParser catch a missing comma offline, but not a column that doesn’t exist or a string compared to a number. That needs a catalog: Apache Calcite and ZetaSQL validate against one you supply, sqlglot against a schema you hand it, sqlc against your DDL, and SQLx in offline mode against cached query metadata. ↩︎
