Inside TypeDB Studio: Part 2 - Querying

TypeDB Studio is the official interactive environment for TypeDB – a database that reimagines data modelling around entities, relations, and attributes rather than tables or documents. Studio gives developers a visual, multi-modal interface for building TypeQL queries, visualising schema and exploring data instances, all from the browser or as a native desktop app.
This is the next post in the TypeDB Studio internals series, following on from Part 1: Architecture, and dives deeper into how Studio manages TypeDB Server connections, sends queries, and processes responses.
TypeDB Studio is fully open source, and you can browse its source code at https://github.com/typedb/typedb-studio.
Client-Server Connection Architecture
Studio communicates with TypeDB via @typedb/driver-http, an HTTP-based driver. The TypeDB HTTP API is intended as a lightweight, portable alternative to its high-performance gRPC server . The gRPC protocol built on HTTP/2 is unfortunately still not supported in web browsers at the time of writing.
Both client-server connection state and querying operations are centralised in the DriverState service in TypeDB Studio. An initial call to auth exchanges login credentials for a short-lived token. The DriverState service uses this short-lived token to perform database operations.
All TypeDB queries require a transaction, but Studio needs to be easy for everyone to use while also being versatile enough for more advanced use cases. As such, we expose two transaction modes: auto and manual.
Automatic transaction management
Auto mode (the default transaction mode) uses oneShotQuery() – a single HTTP call that opens a transaction, executes the query, and commits or closes in one round-trip. A QueryTypeDetector first tries a read transaction; if TypeDB signals that a write is needed, it escalates by retrying with a write transaction. Only the successful attempt is logged.
Auto mode is the default for all users as it helps achieve Studio’s goal of making TypeDB easy for everyone to use – you run queries and they “just work”.
Manual transaction management
Manual mode exposes the full transaction lifecycle: openTransaction() → query() → commitTransaction() / closeTransaction(). This is useful for multi-statement transactions where the user wants explicit control. It is useful also for exploring a specific snapshot of a frequently-mutating database.
Concurrency control
A write-lock semaphore (BehaviorSubject<Semaphore | null>) prevents concurrent operations from interleaving. The semaphore supports nesting, so internal operations that need to run multiple queries (like schema refresh) can acquire and release the lock correctly.
Technically there are some operations that can be executed safely concurrently. It is also not possible to guard against a different client running another operation against the server in parallel. As such, the primary goal of Studio’s concurrency control is maintaining stability of the application while keeping the code simple and not riddled with edge cases.
A summary of TypeDB Studio’s query tools
Before proceeding, let’s review the tools actually available in TypeDB Studio for querying data.
- Code editor (“manual mode”) – Simple interface with syntax highlighting and autocompletion for manually writing and executing TypeQL with output visualised as plaintext or tables.
- Agent mode – Tell the TypeDB AI agent what you want to query, have AI generate your queries and you can run them inline in the chat pane.
- Data explorer
- Graph visualisation
In this post, we’ll cover the architecture of (1) and (2) – manual mode and agent mode.
Manual Mode: Writing TypeQL and Reading Responses

The most basic mode of using TypeDB Studio. Simply open a query tab, write your TypeQL query, run it and get answers. Query tabs are saved to local storage (unless you close the tab).
Part 1: Architecture covers the architecture of the query editor in great detail. I will briefly cover log and table outputs here.
When you run a query in TypeDB Studio, the same API response fans out to all output modes simultaneously, and each one transforms it independently.
Log output is the simplest: an append-only FormControl<string> bound to a read-only <textarea>. As results arrive, they’re formatted into aligned text lines (column headers, concept rows, timestamps) and appended sequentially. It reads like a terminal session.
Table output is reactive. It maintains a BehaviorSubject<TableRow[]> feeding an Angular Material <mat-table>. The columns are not hardcoded, but rather discovered dynamically from the query response’s variable names when the first result arrives. Row insertion is asynchronous to keep the UI responsive.
Schema tool window
The schema tool window sits side-by-side with the query editor, showing your schema’s type hierarchy and – when expanded – the capabilities of individual entities and relations in the schema. This is intended to be used for assistance while writing queries.
Schema queries
The schema tool window is backed by a SchemaState service that fetches and caches the database’s type hierarchy. On connection (or when the user triggers a refresh), it runs the four following queries:
# 1. Type hierarchy (all types and their direct supertypes)
match { $t sub! $supertype; } or { $t sub $supertype; $t is $supertype; };
# 2. Owned attributes (excluding inherited)
match { $t owns $attr; not { $t sub! $sown; $sown owns $attr; }; };
# 3. Related roles (excluding inherited)
match { $t relates $related; not { $t sub! $srel; $srel relates $related; }; };
# 4. Played roles (excluding inherited)
match { $t plays $played; not { $t sub! $splay; $splay plays $played; }; };
Rendering query responses as a tree

The raw response is transformed into a tree structure: each type becomes a node whose children are its subtypes. The tree is rendered using an Angular MatTree. In the default hierarchical structure, the three root nodes are always entity, relation and attribute. Capabilities (owns, plays, relates) are listed as leaf entries under each type and can be toggled on or off globally via the actions menu.
The schema tree is constructed using the following algorithm:
1. populateConcepts(): Iterate the type hierarchy query results and create a flat lookup map for each kind — entityTypes, relationTypes, attributeTypes — keyed by label.
2. buildTypeHierarchy(): (only run if hierarchical view is enabled) Iterate the same type hierarchy results again. For each (type, supertype) pair where the labels differ (filtering out self-referential root types), look up both nodes in the maps, set node.supertype = supernode, and push the node into supernode.subtypes[]. This wires up the parent-child links.
3. attachOwnedAttributes(), attachPlayedRoles(), attachRelatedRoles(): Iterate the query results of the relevant TypeQL query, find the ‘owner’ node, and call a recursive propagate*() method. These methods push the capability onto the node and then recurse into all subtypes, so every node ends up with the full set of capabilities it has access to, whether declared or inherited.
Finally there is the “output as plaintext” action, which simply calls the getDatabaseSchema() method from @typedb/driver-http which returns the plaintext schema. Easy!
Reading results as plaintext (Log view)

The default output format is a plain-text log. It logs when the query starts running and when the results come back from the server. Results are collected eagerly, not streamed (only the gRPC-based TypeDB drivers support streaming). TypeDB Studio sets a default limit of 100 results to avoid consuming resources unnecessarily; you can configure this.
For Fetch queries, results are JSON objects. For Select queries, they are rows.
Reading results as a table

Tabular output is an effective way to read rows (from a select) and flat JSON objects from a fetch. Note that nested JSON fields will be rendered as plain JSON objects.
Agent Mode: Streaming AI with Inline Query Execution

Agent mode is a conversational interface where users describe what they want in natural language, receive AI-generated TypeQL, and execute it in place – all without leaving the chat. It uses streaming HTTP parsing, a multi-part message model, and the same graph/table/log output infrastructure used by the query page.
Messages are not plain text. Each message is a list of typed parts:
type MessagePart = MessagePartText | MessagePartCode | MessagePartOutput;
A MessagePartText holds markdown. A MessagePartCode holds an editable FormControl<string> with a language tag. A MessagePartOutput holds the same RunOutputState structure used by the query page – log, table, graph, and raw views, each with its own rendering state.
This model means a single AI response can contain interleaved prose and code blocks, and each code block can accumulate multiple execution runs with independent output tabs. The output block is inserted into the message’s content array immediately after its code block, so the relationship is positional rather than referential.
Streaming Markdown Parser
AI responses arrive as Server-Sent Events from a cloud endpoint. The StreamingMarkdownParser is a stateful, incremental parser that processes chunks as they arrive while searching specifically for code blocks (because these are richly rendered):
- Each chunk is appended to an internal buffer.
- The parser scans for triple-backtick code fences, tracking whether it is inside or outside a code block.
- On each call to
parseChunk(), it returns the full list of completed parts plus a partial in-progress part (either trailing text or an incomplete code block). - On stream completion,
flush()finalizes any remaining buffer content.
Automatic chat compaction
When the conversation grows too large, Studio automatically triggers a compaction: it sends the conversation history to a /compact endpoint, which returns a summarized version. This is similar to how Claude Code behaves.
The previous history of the conversation is retained – the user should be able to view it all. However, when prompting the agent, we only send the segment of the conversation since the last compaction.
Conversation Persistence
Conversations are stored per-database in localStorage. Only text and code parts are persisted – output blocks are ephemeral. This keeps storage compact and avoids serializing graph state. When a conversation is restored, code blocks get fresh FormControl instances, and output blocks are recreated on re-execution.
Conclusion
TypeDB Studio offers a full query editor with schema-aware autocomplete and a streaming AI agent that generates and executes TypeQL inline. Where possible, we’ve gone for deliberate architectural choices to keep things simple – a unified RunOutputState powering outputs across both manual and agent mode; centralising all TypeDB Server communications in a shared DriverState. The result is a querying tool that hopefully is powerful and intuitive enough for all Studio users – and for true power users, we hope the source code serves as a valuable reference point for users building their own TypeDB tooling.
We haven’t covered everything yet. Graph visualisation, the data explorer, and the schema tool window each have their own interesting design decisions, and we’ll dig into those very soon.
Try TypeDB Studio
- https://studio.typedb.com – Open the TypeDB Studio web app
- https://typedb.com/docs/home/install/studio – Install the TypeDB Studio desktop app
- https://typedb.com/docs/tools/studio – Read the TypeDB Studio docs
- Get in touch with the TypeDB team for any enquiries you may have
- Connect with us on Discord
