Using TypeDB
You are about to interact with a TypeDB 3.x knowledge graph through the typedb MCP tool family. This skill exists because most TypeQL knowledge in your training data is for TypeDB 2.x, and TypeQL 3.x is a different language. Treat your priors as stale and verify against the live schema.
Announce: "I'm using the using-typedb skill to interact with the TypeDB database."
The cardinal rules
- TypeDB 3.x is not 2.x. If you remember TypeQL from training, assume it is wrong until verified. Notable changes:
getis gone (usefetch),sub entity/sub relation/sub attributeis gone for top-level type declarations (you writeentity persondirectly),abstractis now@abstract,regexis now@regex(...), cardinality is@card(...), enums are@values(...). - Always fetch the schema before writing a query. Never write TypeQL from memory of "what the schema probably is." Use
typedb-database_schemafirst. - Pick the right transaction type or nothing works.
readformatch/fetch,writeforinsert/update/delete,schemafordefine/redefine/undefine. The MCP error when you get this wrong is not always obvious — this is the #1 first failure for new agents. - Verify every write. After an insert or delete, run a small
match … fetch { $x.* };to confirm the data actually lives. "Ran the write, didn't verify" is a failure mode, not a workflow. - Never
database_deletewithout explicit user permission, and never run aschematransaction on a production database without first stating what you are about to change.
Workflow
Step 1 — read the schema
typedb-database_schema(name="<db>")
Returns the full define text. Read it. Identify the types, their attributes, their relations, the roles in each relation, and the cardinality/value annotations. Only then write a query.
Step 2 — write the query against the actual schema
Use TypeQL 3.x. The patterns below are verified against the 3.x docs.
Fetch all attributes of matched entities:
match $x isa person;
fetch { $x.* };
Fetch specific attributes and nest sub-queries:
match
$u isa user, has username $name;
fetch {
"name": $name,
"all_attributes": { $u.* },
"friends": [
match friendship ($u, $other);
fetch { "friend": $other.username };
]
};
Insert an entity:
insert
$p isa person, has name "Alice", has email "alice@example.com";
Insert a relation by binding role players:
insert
$r isa friendship (friend: $a, friend: $b);
(You'll need a match clause before insert to bind $a and $b to existing entities.)
Delete an attribute (inverted has):
match $p isa person, has email $e;
delete has $e of $p;
Delete an entity:
match $p isa person, has name "Alice";
delete $p;
Step 3 — verify
After any write, run a read query that would observe what you just wrote. If the row isn't there, the transaction didn't commit or your match pattern is wrong — investigate before continuing.
Transaction types — table
| Operation | transaction_type |
|---|---|
match … fetch …; |
read |
match … return …; (aggregations) |
read |
insert …; |
write |
match … insert …; |
write |
match … delete …; |
write |
define …; / redefine …; / undefine …; |
schema |
Running a define under write fails. Running an insert under schema fails. There is no auto-detection.
Schema changes — define vs redefine vs undefine
defineadds new types, attributes, or relations. Idempotent for additions.redefinecan change annotations only (e.g. swap@card(0..1)for@card(1..1), change@regex). It cannot change structural declarations likesub,owns,plays, orrelates.undefineremoves a declaration. To change a structural declaration, you mustundefineit and thendefinethe new shape — in that order, in separate statements (often in two separate schema transactions if data depends on the old shape).
If you find yourself wanting redefine entity foo, owns bar; to change the cardinality of bar, you actually want to redefine the annotation on the owns line — not the structural relationship.
The 3.x gotchas that bite
Naming conventions
TypeDB convention is kebab-case for types (asset-tag, strategy-outcome-link, ost-node) and snake_case for attributes (hypothesis_text, created_at, lifecycle_state). LLMs default to camelCase; do not. Match the casing in the live schema exactly.
Attribute subtyping
In 3.x, attributes can be subtyped to enforce ID-shape distinctions:
attribute ost_id @abstract, value string;
attribute outcome_id, sub ost_id, value string @regex("^O_[0-9]{4}$");
attribute tactic_id, sub ost_id, value string @regex("^T_[0-9]{4}$");
An entity that owns outcome_id cannot accidentally has tactic_id "O_0001" — the type system enforces shape. This is intentional. Honor it: when inserting, use the right subtype's value, and trust regex enforcement to catch mistakes.
Regex double-escaping through MCP
Regexes pass through MCP → JSON → TypeQL and get double-escaped. A regex containing \s will arrive at TypeDB as \\\\s if you escape naively. Test patterns end-to-end before relying on them, and prefer permissive patterns (.+, .*) over character classes that need escapes. This is a real bug we've hit.
Cardinality on relation roles
Relations declare per-role cardinality:
relation tactic-task,
relates tactic @card(1..1),
relates task @card(1..1);
@card(1..1) means exactly one role player. To delete an entity that plays a @card(1..1) role, you must delete the relation first, then the entity — otherwise the cardinality constraint is violated mid-delete. Two-stage delete:
# Stage 1: remove the relation
match
$t isa task, has task_id "K_00000001";
$r isa tactic-task (task: $t);
delete $r;
# Stage 2: remove the entity
match $t isa task, has task_id "K_00000001";
delete $t;
@values(...) enums
attribute lifecycle_state, value string @values("hypothetical", "validated", "falsified");
Inserting any other string fails. Don't invent new states — extend the schema deliberately if you need a new one, and surface that decision to the human user.
Roles are scoped to relations
A role like outcome in strategy-outcome-link is not the same thing as a role outcome in some other relation. When binding role players in an insert, the relation type determines which role names are legal:
insert $link isa strategy-outcome-link (strategy: $s, outcome: $o), ... ;
Use the role names exactly as declared in relates clauses in the schema.
Reading the schema critically
When typedb-database_schema returns, scan for:
- Subtype hierarchies (
entity foo @abstractwith concrete subtypes) — the abstract type can't be instantiated; queries for$x isa foowill return all subtypes. @keyvs@card(1..1)—@keyis unique and required;@card(1..1)is required but not unique.- Role-relation graph — list every
plays X:Yon entities and everyrelates Yon relations. That tells you the legal shape of inserts. @regexon IDs — your generated IDs must match, or insert fails.@valuesenums — your strings must be in the allowed set.
When you don't know
- "I don't know what type to use here — let me read the schema first" is correct.
- "The schema doesn't have a place for this concept" is a real answer; surface it to the user rather than inventing types.
- "I tried this and the MCP returned an error" — paste the actual error to the user; do not paper over it with a different query.
Don'ts
- Don't
database_deletewithout explicit permission. - Don't run
schematransactions on production without stating the diff first. - Don't write queries from memory of the schema.
- Don't assume 2.x syntax works.
- Don't claim a write succeeded without a verifying read.
- Don't invent new enum values or asset kinds — extend the schema deliberately.