TypeDB basics
TypeDB stores data as typed entities, relations and attributes, and queries it with TypeQL, a declarative pattern language. This chapter covers what you need to read and write TypeDB 3 schemas and queries.
Running a server
The quickest way to get a server is the official Docker image:
docker run -d --name typedb -p 1729:1729 typedb/typedb:3.14.0
It listens on port 1729. The default credentials are admin / password. A server holds many databases, and every database has its own schema and data. All examples in this chapter use one small publishing domain: people write works, books are reviewed, and authorships win awards.
The model
TypeDB's data model has three kinds of type. They are often summarized as the PERA model (polymorphic entity–relation–attribute).
Entities
Things that exist on their own: a person, a book. An entity is identified by the database, not by a primary key column.
Relations
Things that exist because they connect other things: an authorship connects an author and a work. Each participant fills a named role.
Attributes
Typed values that instances own: a name string, a year integer. An attribute is an instance in its own right and can be owned by many instances.
Some properties of this model are different from tables and from property graphs:
- Relations are n-ary. A relation can have any number of roles and any number of players in each role. A three-way
reviewwith a reviewer, a subject and an editor is one relation, not three edges. - Relations are first-class. They can own attributes (a review has a
rating) and can themselves play roles in other relations (anawardis won by an authorship, the fact that a particular person wrote a particular book). This is called reification, and in TypeDB it needs no workaround. - Attributes are shared values. If two books own the genre
"lean", they own the same attribute instance. You can match on it from either side. - Types are polymorphic. With
sub, abookis awork. A query for works finds books and articles, and an owned attribute or a played role is inherited by subtypes.
Defining a schema
A schema is written with define. Statements end with a semicolon, and the properties of one type are separated by commas:
define
attribute name, value string;
attribute email, value string;
attribute isbn, value string;
attribute title, value string;
attribute year, value integer;
attribute genre, value string;
attribute rating, value integer;
attribute prize, value string;
entity person,
owns email @key,
owns name @card(1..1),
plays authorship:author,
plays review:reviewer;
entity work @abstract,
owns title @card(1..1),
plays authorship:work;
entity book sub work,
owns isbn @key,
owns year,
owns genre @card(0..),
plays review:subject;
entity article sub work;
relation authorship,
relates author @card(1..),
relates work,
plays award:winner;
relation review,
relates reviewer,
relates subject,
owns rating;
relation award,
relates winner,
owns prize;
Read it as a set of facts about types:
ownssays which attributes instances of a type may have.plays authorship:authorsays which roles they may fill. A role is always named by its relation:authorship:author.relatesdeclares a relation's roles.book sub workmakesbooka subtype. It inheritsowns titleandplays authorship:workfromwork.@abstractmeansworkhas no direct instances; only its subtypes do.authorship ... plays award:winnerlets a relation be a role player.
A schema can be extended later with more define statements, and changed with undefine and redefine.
Annotations and value types
| Annotation | Meaning |
|---|---|
@key | Exactly one value, unique among instances of the owner type. Used to look an instance up. |
@card(1..1) | Cardinality: how many values an instance owns, or how many players a role has. @card(0..) is unbounded. The default for owns is @card(0..1). |
@unique | A value may be owned by at most one instance, without being required. |
@abstract | The type cannot have direct instances. |
Attribute types declare one value type each:
| Value type | Example literal |
|---|---|
boolean | true |
integer | 2026 (64-bit signed) |
double | 3.14 |
decimal | 12.50dec (exact, fixed point) |
string | "Typed Graphs" |
date | 2026-10-29 |
datetime | 2026-10-29T09:30:00 |
datetime-tz | 2026-10-29T09:30:00 Europe/Kyiv or …+02:00 |
duration | P1Y2M3DT4H (ISO 8601) |
Transactions
Every query runs inside a transaction, and a transaction has one of three kinds:
- read:
match,fetch,reduceand other queries that do not change anything. - write: also
insert,delete,updateandput. Changes become visible when the transaction commits. - schema: also
define,undefineandredefine. A schema transaction may write data too, so a schema change and a data backfill can commit together.
Transactions are isolated. When two write transactions change the same data, the one that commits second fails with a conflict and can be retried. typedb-lean puts the transaction kind into the type of a transaction body (chapter 5).
Writing data
insert creates instances. Variables start with $. isa gives the type, has adds an attribute, and links connects role players to a relation:
insert
$ann isa person, has email "ann@example.com", has name "Ann";
$bob isa person, has email "bob@example.com", has name "Bob";
$b isa book, has isbn "978-1", has title "Typed Graphs", has year 2026,
has genre "databases", has genre "lean";
$a isa authorship, links (author: $ann, author: $bob, work: $b);
$r isa review, links (reviewer: $bob, subject: $b), has rating 5;
The authorship has two players in the author role, which @card(1..) allows. To connect data that already exists, put a match before the insert. The insert runs once for every answer of the match:
match
$b isa book, has isbn "978-1";
$a isa authorship, links (work: $b);
insert
$w isa award, links (winner: $a), has prize "Best Typed Book";
Here a relation, the authorship, plays the winner role of another relation.
Reading data
A match is a pattern. Its answers are all the ways to bind its variables so that every statement holds. Statements are joined by ;, which means "and":
match
$p isa person, has name $n;
$a isa authorship, links (author: $p, work: $w);
$w has title $t;
This returns rows with columns $p, $n, $a, $w and $t. Note that $w was never given a type. TypeDB infers types from the schema: only a work can play authorship:work, so $w is a work, and it finds books and articles. Type inference works without any isa at all:
match $x has name $n; # $x: person (the only owner of name)
match $r links (author: $p); # $r: authorship, $p: person
The server rejects a pattern whose variables can have no type at all. A misspelled role, or an attribute the type does not own, fails before the query runs:
match $b isa book, has rating $r;
[INF11] Type-inference was unable to find compatible types for the pair of
variables 'b' & 'r' across a 'has' constraint. Types were:
- b: [book]
- r: [rating]
The same check guards writes. An insert that gives a person an isbn, or puts a book into the author role, is rejected with the same kind of error.
Optional and negated parts
try { … } makes part of a pattern optional; its variables are empty when it does not match. not { … } excludes answers. or takes alternatives:
match $p isa person, has name $n; try { $p has email $e; };
match $p isa person; not { $a isa authorship, links (author: $p); };
Comparisons and aggregates
Values can be compared, and reduce aggregates the answers, optionally per group:
match $b isa book; reduce $count = count;
match $p isa person; $a isa authorship, links (author: $p); reduce $works = count groupby $p;
sum, max, min, mean and others work the same way. A query can also be followed by sort, offset, limit and select.
Fetching documents
Rows are good for programs that work with concepts. When you want JSON, end the query with fetch, which turns each answer into a document. $b.title reads an attribute; brackets collect several values; a nested query builds a sub-list:
match $b isa book, has year $y; $y > 2020;
fetch { "title": $b.title, "genres": [ $b.genre ] };
match $p isa person;
fetch {
"name": $p.name,
"works": [ match $a isa authorship, links (author: $p, work: $w);
fetch { "title": $w.title }; ]
};
{"title": "Typed Graphs", "genres": ["databases", "lean"]}
{"name": "Ann", "works": [{"title": "Typed Graphs"}]}
fetch { $p.* } returns all of an instance's attributes.
Changing data
update replaces an attribute that has at most one value. put inserts only when the pattern does not already exist, so running it twice is safe. delete removes instances, ownerships or role players:
match $p isa person, has email "ann@example.com";
update $p has name "Annie";
put $p isa person, has email "cy@example.com", has name "Cy";
match $r isa review, has rating $x;
delete has $x of $r;
match $p isa person, has email "cy@example.com";
delete $p;
Functions
Functions name a reusable pattern. They are part of the schema, and they can return a stream of answers or a single value:
define
fun works_of($p: person) -> { work }:
match
$a isa authorship, links (author: $p, work: $w);
return { $w };
fun book_count($p: person) -> integer:
match
$a isa authorship, links (author: $p, work: $w);
$w isa book;
return count;
Call them inside patterns with let:
match $p isa person, has name "Ann"; let $w in works_of($p); $w has title $t;
match $p isa person, has name "Ann"; let $c = book_count($p);
Functions can call themselves, which gives you recursive queries. With a follows relation between people, everyone reachable through follows is:
define
relation follows, relates follower, relates followed;
person plays follows:follower, plays follows:followed;
fun reachable($p: person) -> { person }:
match
{ $f isa follows, links (follower: $p, followed: $q); } or
{ $f isa follows, links (follower: $p, followed: $m); let $q in reachable($m); };
return { $q };
Every query on this page was run against TypeDB 3.14.0. The TypeDB documentation has the full TypeQL reference.
TypeDB for Edge AI Agents covers TypeDB's model and TypeQL in depth, including modeling knowledge for agents, functions and inference patterns.