This tutorial demonstrates how to build a task queue using WOQL RDF list operations. You'll learn how to create, modify, and query ordered lists stored as document properties.
With the built in plain JSON support (sys:JSONDocument and sys:JSON data structure) and these list operators, queue operations can be performed directly on JSON lists created using the JSON documents interface using advanced declarative logic on the server side with ACID transactions.
Prerequisites
- Node.js 16+
- TerminusDB running on
localhost:6363 terminusdbinstalled
Document-Based Task Queue
This example creates a TaskQueue document with an ordered list of tasks, then demonstrates all the list operations.
1. Install TerminusDB (or use cloud with token)
Get TerminusDB running on localhost:6363 (see Quickstart Guide)
To just start it:
docker run --pull always -d -p 127.0.0.1:6363:6363 -v terminusdb_storage:/app/terminusdb/storage --name terminusdb terminusdb/terminusdb-server:v122. Install javascript TerminusDB client
npm install terminusdb3. Create the script, including Setup and Schema
Combine all the sections together, or run it step by step.
import { WOQLClient, WOQL } from "terminusdb";
const client = new WOQLClient("http://localhost:6363", {
user: "admin",
organization: "admin",
key: "root",
});
// Create database
await client.createDatabase("task_queue_demo", {
label: "Task Queue Demo",
comment: "Demonstrates rdflist operations with documents",
});
client.db("task_queue_demo");
// Define schema with a TaskQueue document containing a List
const schema = [
{
"@type": "Class",
"@id": "TaskQueue",
"@key": { "@type": "Lexical", "@fields": ["name"] },
"name": "xsd:string",
"tasks": { "@type": "List", "@class": "xsd:string" }
}
];
await client.addDocument(schema, { graph_type: "schema" });Create Initial Queue
// Create a task queue document with initial tasks
const taskQueue = {
"@type": "TaskQueue",
"name": "MyQueue",
"tasks": ["Setup environment", "Write tests", "Deploy"]
};
await client.addDocument(taskQueue);
// Get the list head from the document for list operations
const getListHead = WOQL.triple("TaskQueue/MyQueue", "tasks", "v:listHead");
const result = await client.query(getListHead);
const listHead = result.bindings[0]["listHead"];
console.log("List head:", listHead);Read Operations
// Peek at the first task (without removing)
const peekResult = await client.query(
WOQL.lib().rdflist_peek(listHead, "v:first")
);
console.log("First task:", peekResult.bindings[0]["first"]["@value"]);
// Output: "Setup environment"
// Get the queue length
const lengthResult = await client.query(
WOQL.lib().rdflist_length(listHead, "v:len")
);
console.log("Queue length:", lengthResult.bindings[0]["len"]["@value"]);
// Output: 3
// Get all tasks as an array
const listResult = await client.query(
WOQL.lib().rdflist_list(listHead, "v:tasks")
);
const tasks = listResult.bindings[0]["tasks"].map(t => t["@value"]);
console.log("All tasks:", tasks);
// Output: ["Setup environment", "Write tests", "Deploy"]
// Iterate through tasks with member
const memberResult = await client.query(
WOQL.lib().rdflist_member(listHead, "v:task")
);
memberResult.bindings.forEach((b, i) => {
console.log(`Task ${i}: ${b["task"]["@value"]}`);
});
// Get a slice (positions 0-2, exclusive end)
const sliceResult = await client.query(
WOQL.lib().rdflist_slice(listHead, 0, 2, "v:slice")
);
const slice = sliceResult.bindings[0]["slice"].map(t => t["@value"]);
console.log("First two tasks:", slice);
// Output: ["Setup environment", "Write tests"]Add Tasks
// Push a task to the front (high priority)
await client.query(
WOQL.lib().rdflist_push(listHead, WOQL.string("URGENT: Fix bug"))
);
// Append a task to the end
await client.query(
WOQL.lib().rdflist_append(listHead, WOQL.string("Cleanup"), "v:newCell")
);
// Insert at a specific position (index 2)
await client.query(
WOQL.lib().rdflist_insert(listHead, 2, WOQL.string("Code review"))
);
// Verify the queue
const updatedResult = await client.query(
WOQL.lib().rdflist_list(listHead, "v:tasks")
);
console.log(updatedResult.bindings[0]["tasks"].map(t => t["@value"]));
// Output: ["URGENT: Fix bug", "Setup environment", "Code review", "Write tests", "Deploy", "Cleanup"]Process Tasks
// Pop the first task (removes and returns it)
const popResult = await client.query(
WOQL.lib().rdflist_pop(listHead, "v:task")
);
console.log("Processing:", popResult.bindings[0]["task"]["@value"]);
// Output: "URGENT: Fix bug"
// Drop task at position 1 (removes without returning)
await client.query(
WOQL.lib().rdflist_drop(listHead, 1)
);
// Check remaining tasks
const afterOps = await client.query(
WOQL.lib().rdflist_list(listHead, "v:tasks")
);
console.log(afterOps.bindings[0]["tasks"].map(t => t["@value"]));
// Output: ["Setup environment", "Write tests", "Deploy", "Cleanup"]Reorder Tasks
// Swap positions: move "Deploy" (index 2) to front (index 0)
await client.query(
WOQL.lib().rdflist_swap(listHead, 2, 0)
);
// Reverse the entire queue
await client.query(
WOQL.lib().rdflist_reverse(listHead)
);
// Verify order
const reorderedResult = await client.query(
WOQL.lib().rdflist_list(listHead, "v:tasks")
);
console.log(reorderedResult.bindings[0]["tasks"].map(t => t["@value"]));Clear the Queue
// Clear all tasks from the queue
await client.query(
WOQL.lib().rdflist_clear(listHead, "v:empty")
);
// Verify the queue is empty
const isEmptyResult = await client.query(
WOQL.lib().rdflist_is_empty(listHead)
);
const isEmpty = isEmptyResult.bindings && isEmptyResult.bindings.length > 0;
console.log("Queue is empty:", isEmpty);
// Output: trueVerify Changes in Document
The list operations modify the underlying RDF structure. The document API reflects these changes:
const updatedDoc = await client.getDocument({ id: "TaskQueue/MyQueue" });
console.log(updatedDoc.tasks);
// Shows the current state of the tasks listUnderstanding RDF List Internals with Cons Cells
RDF lists are implemented as linked cons cells. Each cell has:
rdf:first- the value at this positionrdf:rest- pointer to the next cell (orrdf:nilfor end)
This example shows how to work directly with the cons cell structure.
Create a List Manually
// Create a list [A, B, C] using cons cells directly
const createList = WOQL.and(
// Generate unique IDs for each cell
WOQL.idgen_random("terminusdb://data/Cons/", "v:cell1"),
WOQL.idgen_random("terminusdb://data/Cons/", "v:cell2"),
WOQL.idgen_random("terminusdb://data/Cons/", "v:cell3"),
// Cell 1: first="A", rest=cell2
WOQL.add_triple("v:cell1", "rdf:type", "rdf:List"),
WOQL.add_triple("v:cell1", "rdf:first", WOQL.string("A")),
WOQL.add_triple("v:cell1", "rdf:rest", "v:cell2"),
// Cell 2: first="B", rest=cell3
WOQL.add_triple("v:cell2", "rdf:type", "rdf:List"),
WOQL.add_triple("v:cell2", "rdf:first", WOQL.string("B")),
WOQL.add_triple("v:cell2", "rdf:rest", "v:cell3"),
// Cell 3: first="C", rest=nil (end of list)
WOQL.add_triple("v:cell3", "rdf:type", "rdf:List"),
WOQL.add_triple("v:cell3", "rdf:first", WOQL.string("C")),
WOQL.add_triple("v:cell3", "rdf:rest", "rdf:nil")
);
const result = await client.query(createList);
const listHead = result.bindings[0]["cell1"];Visualizing the Structure
listHead (cell1)
├── rdf:type → rdf:List
├── rdf:first → "A"
└── rdf:rest → cell2
├── rdf:type → rdf:List
├── rdf:first → "B"
└── rdf:rest → cell3
├── rdf:type → rdf:List
├── rdf:first → "C"
└── rdf:rest → rdf:nilQuery the Structure Directly
// Traverse the list manually using path queries
const traverseQuery = WOQL.and(
WOQL.triple(listHead, "rdf:first", "v:first"),
WOQL.triple(listHead, "rdf:rest", "v:second_cell"),
WOQL.triple("v:second_cell", "rdf:first", "v:second"),
WOQL.triple("v:second_cell", "rdf:rest", "v:third_cell"),
WOQL.triple("v:third_cell", "rdf:first", "v:third")
);
const traverseResult = await client.query(traverseQuery);
const { first, second, third } = traverseResult.bindings[0];
console.log([first["@value"], second["@value"], third["@value"]]);
// Output: ["A", "B", "C"]Create an Empty List
// An empty list is just a reference to rdf:nil
const createEmpty = WOQL.lib().rdflist_empty("v:emptyList");
const emptyResult = await client.query(createEmpty);
console.log(emptyResult.bindings[0]["emptyList"]);
// Output: "rdf:nil"
// Check if a list is empty
const checkEmpty = WOQL.lib().rdflist_is_empty("rdf:nil");
const isEmptyCheck = await client.query(checkEmpty);
console.log("Is empty:", isEmptyCheck.bindings.length > 0);
// Output: trueWhy Use Library Functions?
The WOQL.lib().rdflist_* functions handle:
- Automatic cons cell creation with unique IDs
- Proper linking of
rdf:restpointers - Cleanup of removed cells
- Edge cases (empty lists, single elements)
Manual cons cell manipulation is error-prone. Use the library functions for reliability.
Complete Runnable Example
Save this as task-queue-demo.js:
const TerminusClient = require("terminusdb");
const WOQL = TerminusClient.WOQL;
async function main() {
const client = new TerminusClient.WOQLClient("http://localhost:6363", {
user: "admin",
organization: "admin",
key: "root",
});
// Setup database
try { await client.deleteDatabase("task_queue_demo"); } catch (e) {}
await client.createDatabase("task_queue_demo", {
label: "Task Queue Demo",
comment: "RDF list operations demo",
});
client.db("task_queue_demo");
// Create schema
await client.addDocument([{
"@type": "Class",
"@id": "TaskQueue",
"@key": { "@type": "Lexical", "@fields": ["name"] },
"name": "xsd:string",
"tasks": { "@type": "List", "@class": "xsd:string" }
}], { graph_type: "schema" });
// Create document with initial tasks
await client.addDocument({
"@type": "TaskQueue",
"name": "MyQueue",
"tasks": ["Task 1", "Task 2", "Task 3"]
});
// Get list head
const headResult = await client.query(
WOQL.triple("TaskQueue/MyQueue", "tasks", "v:listHead")
);
const listHead = headResult.bindings[0]["listHead"];
console.log("List head:", listHead);
// Demonstrate operations
console.log("\n--- Initial State ---");
let tasks = await getTaskList(client, listHead);
console.log("Tasks:", tasks);
console.log("\n--- Push to front ---");
await client.query(WOQL.lib().rdflist_push(listHead, WOQL.string("Urgent")));
tasks = await getTaskList(client, listHead);
console.log("Tasks:", tasks);
console.log("\n--- Append to end ---");
await client.query(WOQL.lib().rdflist_append(listHead, WOQL.string("Final"), "v:c"));
tasks = await getTaskList(client, listHead);
console.log("Tasks:", tasks);
console.log("\n--- Pop from front ---");
const pop = await client.query(WOQL.lib().rdflist_pop(listHead, "v:t"));
console.log("Popped:", pop.bindings[0]["t"]["@value"]);
tasks = await getTaskList(client, listHead);
console.log("Tasks:", tasks);
console.log("\n--- Swap positions 0 and 2 ---");
await client.query(WOQL.lib().rdflist_swap(listHead, 0, 2));
tasks = await getTaskList(client, listHead);
console.log("Tasks:", tasks);
console.log("\n--- Reverse ---");
await client.query(WOQL.lib().rdflist_reverse(listHead));
tasks = await getTaskList(client, listHead);
console.log("Tasks:", tasks);
console.log("\n--- Document reflects changes ---");
const doc = await client.getDocument({ id: "TaskQueue/MyQueue" });
console.log("Document tasks:", doc.tasks);
}
async function getTaskList(client, listHead) {
const result = await client.query(WOQL.lib().rdflist_list(listHead, "v:t"));
return result.bindings[0]["t"].map(t => t["@value"]);
}
main().catch(console.error);Run with:
node task-queue-demo.jsRead More
- RDF List Operations Overview - All operations reference
- List Creation - Creating and checking empty lists
- List Access - Reading list elements
- List Modification - Adding and removing elements
- List Transformation - Reordering lists
- W3C RDF Schema - rdf:List - RDF list specification