WOQL (Web Object Query Language) is a declarative query language for TerminusDB. Instead of telling the database how to find data step by step, you describe what you want and the engine figures out the rest.
This tutorial teaches WOQL from scratch through interactive examples you can edit and run directly from this page. Each step builds on the last, introducing one concept at a time.
What you'll learn:
- How variables and binding work — the core mental model
- How
andandorcombine constraints - How to read, insert, update, and delete documents
- How to filter, sort, group, and shape results
Before You Start
Start TerminusDB using Docker:
docker run --rm -p 6363:6363 terminusdb/terminusdb-serverThat's it! The first examples (Steps 1-6) work against the _system database and don't need any setup. They're pure logic exercises.
When you reach Step 7 (reading triples from actual data), you'll need to create the tutorial database. Run the three setup steps below in order before continuing to Step 7.
Setup Step 1: Create the Database
Creates the woql_tutorial database. Run this first.
{
"label": "WOQL Tutorial",
"comment": "Interactive tutorial database"
}Setup Step 2: Add the Schema
Adds a Person class with name, age, city (optional), and email (optional).
{
"@type": "Class",
"@id": "Person",
"@key": {
"@type": "Lexical",
"@fields": [
"name"
]
},
"name": "xsd:string",
"age": "xsd:integer",
"city": {
"@type": "Optional",
"@class": "xsd:string"
},
"email": {
"@type": "Optional",
"@class": "xsd:string"
}
}Setup Step 3: Insert Sample Data
Adds five people: Alice, Bob, Carol, David, and Eve.
[
{
"@type": "Person",
"name": "Alice",
"age": 28,
"city": "New York",
"email": "alice@example.com"
},
{
"@type": "Person",
"name": "Bob",
"age": 35,
"city": "San Francisco",
"email": "bob@example.com"
},
{
"@type": "Person",
"name": "Carol",
"age": 28,
"city": "New York"
},
{
"@type": "Person",
"name": "David",
"age": 42,
"city": "Austin",
"email": "david@example.com"
},
{
"@type": "Person",
"name": "Eve",
"age": 31,
"city": "San Francisco"
}
]Each playground below sends WOQL queries to http://127.0.0.1:6363 with credentials admin:root and database woql_tutorial. Click Settings on any playground to change these.
Phase 1: How WOQL Thinks
Before touching any data, let's understand the core mechanics. These exercises are pure logic — they don't read or write documents. They work against any database.
Step 1: Your First Variable — eq
eq binds a variable to a value. Think of it as declaring "this variable equals this value". The result comes back as one row with the variable filled in.
Variables are written as "v:name". The v: prefix tells WOQL it's a variable, not a literal string.
Run this to see v:greeting bound to 'Hello, WOQL!' in the results.
What happened: WOQL found exactly one way to satisfy the constraint "greeting equals Hello, WOQL!" — by binding v:greeting to that string. One constraint, one solution, one row.
Step 2: Two Variables with and
and means "all of these must hold at the same time". Each constraint adds a new column to the result.
Two eq constraints, two variables, one row where both hold.
What happened: Both constraints are satisfied simultaneously. You get one row with name = Alice and city = New York. The and doesn't multiply rows — it requires all parts to hold in the same solution.
Step 3: Conflicting Constraints — Zero Results
What if you bind the same variable to two different values?
Same variable, two different values. How many solutions exist?
What happened: Zero results. There's no value of v:x that is simultaneously "apple" and "banana". This is constraint satisfaction at work — WOQL doesn't error, it simply finds no valid solutions. This is one of the most important behaviors to internalize.
Step 4: Alternative Solutions with or
or means "at least one of these holds". Each alternative that succeeds produces its own result row.
Two alternatives for the same variable. How many rows come back?
What happened: Two rows — one for each way to satisfy the constraint. or expands the solution space: it says "give me every alternative that works". This is fundamentally different from and, which narrows it.
Key insight: or drives cardinality — the number of result rows. Each successful branch is a separate solution.
Step 5: Controlling Output with select
By default, WOQL returns all variables. select lets you choose which ones appear in the results.
Only v:visible appears in the results, even though v:hidden is also bound.
What happened: Both variables were bound inside the query, but only v:visible made it to the output. select is a filter on what comes back, not on what gets evaluated. The inner query still runs fully — select just hides variables from the result.
Phase 1 Summary
| Concept | What it does |
|---|---|
eq(var, value) | Binds a variable to a value |
and(a, b, ...) | All constraints must hold — narrows solutions |
or(a, b, ...) | Any alternative can hold — expands solutions |
select(vars..., query) | Controls which variables appear in results |
| Conflicting constraints | Produce zero results, not errors |
You now understand the core execution model. Every WOQL query, no matter how complex, is built from these primitives.
Phase 2: Working with Data
Now let's query the Person documents you inserted during setup.
Step 6: The Vars Helper and Dot Syntax
So far we've written variables as raw strings like "v:name". In practice, the JavaScript client provides a Vars helper that lets you declare variables up front and reference them with dot notation. This is cleaner and less error-prone.
Vars creates an object. v.greeting is equivalent to "v:greeting".
Vars("greeting", "audience") returns { greeting: "v:greeting", audience: "v:audience" }. The dot operator is just regular JavaScript property access — nothing magic. You can mix both styles freely, but dot notation catches typos at write time and reads more naturally.
From here on, the examples use whichever style fits best: "v:name" for one-off variables, Vars + dot for multi-variable queries.
Step 7: Reading Triples — triple
Every document in TerminusDB is stored as a set of triples: subject–predicate–object. The triple predicate matches these.
Find every person's name. v:person gets the document ID, v:name gets the value.
What happened: WOQL scanned all triples where the predicate is name and returned each match as a row. The v:person variable holds the document identifier (like Person/Alice), and v:name holds the string value.
Triples are the atoms of TerminusDB. Documents are assembled from triples, and triple is how WOQL accesses individual properties.
Step 8: Joining Triples — Shared Variables
When two triple calls share a variable, WOQL naturally joins them — like a SQL join, but implicit. This is where dot syntax shines: the shared variable is obvious at a glance.
v.person appears in both triples, so they match on the same document.
What happened: Because v.person appears in both triple calls, WOQL ensures they refer to the same document. For each person, you get their name and their age in the same row. This is a natural join — no explicit JOIN keyword needed. This is also called "unification".
Step 9: Type Checking with isa
isa checks if a document is of a specific type. Useful when your database has multiple classes.
Only match documents that are of type Person.
Step 10: Reading Whole Documents — read_document
Instead of extracting individual triples, read_document gives you the full JSON document.
v.doc contains the complete JSON document, not just a single property.
What happened: read_document assembled all the triples for each document into a complete JSON object — including nested subdocuments if any existed. Use triple when you need specific properties for filtering or joining; use read_document when you want the whole thing.
Phase 3: Filtering and Shaping
Step 11: Filtering with Comparisons
greater and less compare values. Combined with triple, they filter results.
Find people whose age is greater than 30.
What happened: WOQL first matched all person names and ages, then kept only the rows where age > 30. The greater predicate acts as a filter — it doesn't generate new bindings, it eliminates rows that don't satisfy the condition.
Step 12: Matching Specific Values
You can match a specific property value by putting a literal in the object position of a triple.
Find people in New York. The literal() wraps the string with its XSD type.
Why literal()? Properties in TerminusDB are stored as typed RDF values. The city field is xsd:string, so matching against it requires a typed literal, not a bare string. literal(value, type) creates the right wrapper.
Step 13: Optional Properties with opt
Some fields are optional — not every Person has an email. opt succeeds even when the inner pattern fails.
Get names and emails. People without email still appear (with email unbound).
What happened: Everyone appears in the results. For people with email, v:email is bound. For people without, it's left unbound (empty). Without opt, people lacking email would be excluded entirely — the triple would fail to match and eliminate that row.
Step 14: Negation with not
not succeeds when the inner pattern fails. Use it to find documents that are missing something.
Find people who do NOT have an email address.
What happened: not inverts the match. The inner triple looks for email — where it succeeds, not fails (excluding that person). Where the inner pattern fails (no email exists), not succeeds. Carol and Eve have no email, so they appear.
Step 15: Combining or with Data
Use or to match documents in multiple cities.
People in New York OR San Francisco. The city appears in the results.
Step 16: Ordering and Limiting
order_by sorts results; limit caps how many come back.
The three youngest people, sorted by age ascending.
Step 17: Grouping and Counting
group_by collects results into groups. Combined with length, it counts.
How many people live in each city?
What happened: group_by groups the inner query results by city, collecting the person IDs into a list per city. Then length counts each group. This is pure WOQL aggregation — no client-side post-processing needed. Why is person empty? It's because it's within the group_by, so it's not part of the outer query. A select could have been used to select only the city_group and city from the group_by.
Phase 4: Writing Data
WOQL queries can read and write in the same transaction. All writes are atomic — they either all succeed or none do. However, all read operations read from the layer that existed when the transaction started, and the writes go to the next transaction. Thus, reads can't see what is being written.
Step 18: Inserting a Document
insert_document adds a new document. The document must conform to the schema.
Insert a new Person. Doc() converts the object to WOQL's internal format. v:id will contain the generated document ID.
After running, try going back to Step 7 and running it again — you should see Frank in the results.
Step 19: Updating a Document
update_document replaces a document. You need to provide the full document with its @id.
Move Alice to Boston and update her age. Run Step 10 again to verify.
Step 20: Deleting a Document
delete_document removes a document by its ID.
Remove Frank. Run Step 7 again to confirm he's gone.
Step 21: Declarative Batch Operations
The real power: combine reading and writing in a single query. Delete all people without an email address in one atomic operation.
Find people without email AND delete them — all in one query. v:name shows who was deleted.
What happened: This is the kind of query that makes WOQL powerful. Instead of fetching IDs in one call and deleting in a loop, you describe the pattern of what to delete and WOQL handles the rest. The read part (isa, not, triple) identifies the targets; the write part (delete_document) acts on them. All atomically.
Where to Go Next
You've learned the fundamentals. Here's where to deepen each area:
| Topic | Page |
|---|---|
| Fluent vs. functional style | WOQL Explanation |
| Data types, dicts, CSV & JSON | Working with Data |
| Variables and unification | What is Unification? |
| Subdocument handling | WOQL Subdocument Handling |
| Path queries (graph traversal) | Path Queries |
| Math operations | Math Queries |
| Schema queries | Schema Queries |
| Range queries | triple_slice Guide |
| Time and date handling | Time Handling |
| Backtracking, scope, and performance | WOQL Control Flow |
| Complete reference | WOQL Class Reference |
Cleanup
To remove the tutorial database when you're done:
Removes the woql_tutorial database and all its data.