Planck Workbench, User Manual
The Workbench is the web-based administration interface for Planck. From a single browser tab you can connect to databases, run PQL queries, deploy and supervise services, monitor performance, manage users, take and restore backups, schedule recurring tasks, and browse logs.
This manual walks through every screen in the order you'll meet it. Sections marked (admin only) are visible only to users with the admin role.
Table of Contents
- Login
- Navigation
- Sidebar, Service Tree
- Query Editor
- Dashboard
- Server Overview
- Services
- Deploying Services
- Backups
- Schedules
- PQL Reference
1. Login
The login screen authenticates you to the System Database.
Fields:
- Admin User: Defaults to
admin - Admin Key: Your Base64-encoded API key
First-time setup: Use the factory default key:
UGxhbmNrX0RlZmF1bHRfQWRtaW5fS2V5XzAwMTA=After the first successful login, the system automatically regenerates your key. A dialog displays the new key, copy and save it, as it cannot be recovered later. Click "Continue" to proceed to the main interface.
Credentials are saved locally for auto-reconnect on subsequent visits.
2. Navigation
The blue header bar at the top provides access to all sections:
| Button | Section | Access |
|---|---|---|
| Query | Query editor workspace | All users |
| Services | Deployed services table | All users |
| Dashboard | Performance monitoring | Admin only |
| Schedules | Automated task management | Admin only |
| Settings | Workbench configuration | Admin only |
| PQL Ref | Query language documentation | All users |
| Logout | End session | All users |
When running in a replica configuration, a role badge (command or query) appears next to the logo.
3. Sidebar, Service Tree
The left sidebar displays the hierarchical service tree:
▼ myapp
● myapp-command (green = running)
users (store)
orders (store)
● myapp-query (green = running)
▼ analytics
○ analytics-svc (grey = stopped)
events (store)Hierarchy:
- Apps, groups of related services
- Services, individual database instances, each with a status dot
- Stores, data namespaces within each service
Status dots: Green = running, Red = crashed, Grey = stopped
Interactions:
- Click a service to open a query tab
- Click a store to pre-fill a query against that store
- Plug icon (hover), connect/disconnect
- Refresh icon (hover), reload schema
- Gear icon (hover), open Server Overview admin panel
4. Query Editor
The query workspace supports multiple tabs, one per service connection.
Tabs
Each tab shows a status dot and the service name. Click + to open a new tab, X to close one. Tabs let you keep parallel sessions against different services.
Editor
Write PQL queries in the upper editor pane. Separate multiple queries with ;.
Execute: click the play button, or press Ctrl+R / Cmd+R.
The editor picks the right query to run automatically:
- If you have text selected, only the selection runs.
- If your cursor sits inside a query (between two
;), that query runs. - Otherwise, the first query in the editor runs.
Results
The lower pane has two views:
Results, a scrollable table. Click any row to expand nested objects. The status bar shows row count, data size, and elapsed time.
JSON, the raw response, pretty-printed. Useful for inspecting errors or copying out a result verbatim.
Status Indicator
- Yellow pulsing dot, query is executing
- Green dot, ready, with the last query's row count, data size, and elapsed time
- Red dot, error, with the message inline
5. Dashboard
Performance monitoring for deployed services. Admin only. 7 tabs:
Overview
KPI cards showing current metrics:
- Total Reads, Writes, Deletes, Updates
- Read/Write Latency (microseconds)
- WAL Bytes Written
- VLog Size
- Dead Ratio (reclaimable space percentage)
Line charts showing trends over time.
Operations
Four charts: DB Operations (reads/writes/updates/deletes), WAL Operations (appends/fsyncs/flushes), Index Operations (inserts/searches/scans), VLog Operations (reads/writes/GC runs).
Latency
Two charts: Read Latency and Write Latency trends over time.
VLogs
Summary cards: Total VLog count, entries, sizes (total/live/dead).
VLog table with columns: ID, Entries, Deleted, Total Size, Live Size, Dead Size, Dead Ratio, Tail flag.
The Dead Ratio column shows a color-coded bar so you can spot reclaim candidates at a glance, green below 40%, orange 40–70%, red at 70% and above. Files in the red band typically benefit most from a GC run (see Schedules).
HTTP Stats
Per-method HTTP request metrics for WASM services. Shows total request count and a table of method, count, average latency, and a distribution bar. Use the auto-refresh control (5s / 10s / 30s / off) to keep the view live during a load test.
WASM Stats
Health and utilization for the WASM instance pool of the selected service:
- Gauge cards for active instances, pool size, and recycled count
- Progress bars showing utilization against the configured maximum
- A pool grid where each slot is colored, green active, blue idle, grey empty, for a quick read of pool pressure
- The configured min and max instance counts
- Average request latency across the pool
Logs
Split-pane log file viewer:
- Left panel: Tree navigation (Apps > Services > Log files)
- Right panel: Line-numbered file viewer with monospace font
- Search: Enter a keyword, matching lines highlighted in yellow
- Log level coloring: ERROR = red, WARN = amber
- Pagination: 1000 lines per page, Previous/Next navigation
6. Server Overview
Full-screen admin panel for a single service. Open via the gear icon in the sidebar.
Schema Tab
Browse stores and indexes:
- Expandable tree listing all stores and their indexes
- Create Store button (admin only)
- Drop Store via right-click context menu
Users Tab (admin only)
Manage database users:
- Table: Username, Role, Actions
- Create User: Username, role selection (admin/read_write/read_only), optional custom key
- Regenerate Key: Generates a new API key, shown in a modal, copy immediately
- Update Role: Inline dropdown per user
- Delete User: With confirmation
Config Tab (admin only)
- YAML editor for the service configuration
- Save / Reset buttons
Backups Tab (admin only)
See Backups section.
Service Controls (top bar)
Running status indicator, Start / Stop / Restart / Undeploy buttons, Back button.
7. Services
Table of all deployed services:
| Column | Description |
|---|---|
| Name | Service identifier |
| App | Parent app name |
| Status | Running (green), Stopped (grey), Crashed (red) |
| Port | TCP port |
| Role | command / query / standalone |
Row actions (hover): Start, Stop, Restart, Undeploy, Overview.
8. Backups
Within the Server Overview panel for each service.
- Create Backup: Enter a directory path, click Create
- Backup List: Table showing path, size, creation timestamp
- Restore: Button per backup (optionally specify target path)
- Delete: With confirmation dialog
9. Schedules
Automated recurring tasks. Admin only.
Task Types
| Type | Description |
|---|---|
| Backup | Periodic backup to specified path |
| GC | Garbage collection, reclaim dead VLog space |
| WAL Truncate | Truncate write-ahead log segments |
| Export | Data export via manifest |
| Import | Data import via manifest |
Creating a Schedule
- Name, Service (dropdown), Task Type
- Cron Expression, text input with preset buttons:
- Every 1 min, Every 5 min, Hourly, Daily 2am, Daily 4am, Weekly Sunday 3am
- Backup Path (for backup tasks)
- Description (optional)
- Enable/disable toggle
Managing Schedules
- Toggle enabled/disabled per task
- Edit any field
- Delete with confirmation
10. PQL Query Language
PQL is Planck's text query language used from the workbench and shell.
Syntax: store.operation(...)
All examples use the AdventureWorks sample dataset (import via the Import feature):
| Store | Key Fields |
|---|---|
orders | EmployeeID (int), CustomerID (int), TotalDue (float) |
employees | EmployeeID (int), Gender (string), MaritalStatus (string) |
products | ProductName (string), ListPrice (float), SubCategoryID (int), MakeFlag (int) |
customers | CustomerID (int), Address.City (string), Address.State (string) |
vendors | VendorName (string), ActiveFlag (int), CreditRating (int) |
productcategories | CategoryName (string) |
1. Count
orders.count()
orders.filter(EmployeeID = 289).count()
vendors.filter(ActiveFlag = 1).count()
products.filter(MakeFlag = 1).count()2. Filter, Equality
employees.filter(Gender = "M").count()
employees.filter(EmployeeID = 274).count()
products.filter(SubCategoryID = 14).count()
productcategories.filter(CategoryName = "Bikes").count()3. Filter, Comparison Operators
Operators: > < >= <= !=
orders.filter(TotalDue > 50000).count()
orders.filter(TotalDue < 100).count()
orders.filter(TotalDue >= 100000).count()
products.filter(ListPrice > 1000).count()
vendors.filter(CreditRating != 1).count()4. Compound Filters (AND)
orders.filter(EmployeeID = 289 and CustomerID = 1045).count()
employees.filter(Gender = "M" and MaritalStatus = "M").count()
orders.filter(EmployeeID >= 285 and EmployeeID <= 287).count()5. Limit & Skip
orders.limit(10).count()
orders.skip(3800).count()
customers.limit(100).count()6. OrderBy (Sorting)
products.orderBy(ListPrice, desc).limit(5)
products.orderBy(ListPrice, asc).limit(5)
employees.orderBy(EmployeeID, asc).limit(3)
employees.orderBy(EmployeeID, desc).limit(3)7. Multi-Sort
orders.orderBy(EmployeeID, asc).orderBy(TotalDue, desc).limit(10)
employees.orderBy(Gender, asc).orderBy(EmployeeID, desc).limit(10)
// With filter
orders.filter(EmployeeID >= 285).orderBy(EmployeeID, asc).orderBy(TotalDue, asc).limit(20)8. Projection (pluck)
Return only specific fields:
employees.filter(EmployeeID = 274).pluck(EmployeeID, FullName)
products.filter(SubCategoryID = 14).limit(1).pluck(ProductName, ListPrice)
employees.limit(1).pluck(EmployeeID)
orders.filter(EmployeeID = 289).orderBy(TotalDue, desc).limit(1).pluck(EmployeeID, TotalDue)9. Aggregation, Count
orders.aggregate(total: count)
orders.filter(EmployeeID = 289).aggregate(total: count)
products.filter(MakeFlag = 1).aggregate(n: count)10. Aggregation, Sum, Avg, Min, Max
orders.aggregate(total: sum(TotalDue))
orders.aggregate(avg_total: avg(TotalDue))
orders.aggregate(min_total: min(TotalDue))
orders.aggregate(max_total: max(TotalDue))
// With filter
orders.filter(EmployeeID = 289).aggregate(revenue: sum(TotalDue))11. GroupBy
orders.groupBy(EmployeeID).aggregate(n: count)
employees.groupBy(Gender).aggregate(n: count)
employees.groupBy(Gender, MaritalStatus).aggregate(n: count)
orders.groupBy(EmployeeID).aggregate(n: count, total: sum(TotalDue))12. Filter + GroupBy
orders.filter(TotalDue > 10000).groupBy(EmployeeID).aggregate(n: count)
products.filter(ListPrice > 0).groupBy(SubCategoryID).aggregate(n: count, avg_price: avg(ListPrice))
orders.filter(EmployeeID = 289).groupBy(CustomerID).aggregate(n: count, total: sum(TotalDue))13. $in Operator
orders.filter(EmployeeID in [289, 288]).count()
orders.filter(EmployeeID in [289, 287, 285]).count()
products.filter(SubCategoryID in [1, 2, 14]).count()
employees.filter(Gender in ["M"]).count()14. $contains Operator
products.filter(ProductName contains "Road").count()
products.filter(ProductName contains "Mountain").count()
products.filter(ProductName contains "Frame").count()
vendors.filter(VendorName contains "Bike").count()15. $startsWith Operator
products.filter(ProductName startsWith "HL").count()
products.filter(ProductName startsWith "Mountain").count()
employees.filter(FirstName startsWith "S").count()16. $exists Operator
products.filter(ProductName exists true).count()
employees.filter(Gender exists true).count()17. $regex Operator
Use ~ as the regex operator:
products.filter(ProductName ~ "^HL").count()
products.filter(ProductName ~ "Frame").count()
products.filter(ProductName ~ "58$").count()
products.filter(ProductName ~ "^AWC Logo Cap$").count()18. OR Filters
employees.filter(Gender = "M" or MaritalStatus = "S").count()
products.filter(ProductName contains "Road" or ProductName contains "Mountain").count()
products.filter(SubCategoryID = 1 or SubCategoryID = 2).count()
orders.filter(TotalDue > 100000 or TotalDue < 100).count()
orders.filter(EmployeeID = 289 or EmployeeID = 288).count()19. Range Scans
// Closed range
orders.filter(EmployeeID >= 285 and EmployeeID <= 287).count()
// Open range
orders.filter(EmployeeID > 285 and EmployeeID < 289).count()
// One-sided
orders.filter(EmployeeID > 288).count()
orders.filter(EmployeeID < 285).count()20. $between Operator
Inclusive range match, equivalent to field >= lower and field <= upper, but more concise and directly mapped to a B+ tree range scan when the field is indexed:
orders.filter(TotalDue between 100 and 5000).count()
products.filter(ListPrice between 10.0 and 50.0).count()
employees.filter(EmployeeID between 280 and 290).count()
// Combined with other conditions
products.filter(ListPrice between 10.0 and 50.0 and MakeFlag = 1).count()
orders.filter(EmployeeID between 285 and 289).orderBy(TotalDue, desc).limit(10)Both bounds are inclusive. Works on numeric fields.
21. Nested Field Access
Use dot notation for embedded documents:
customers.filter(Address.City = "New York").count()
customers.filter(Address.State = "CA").count()
customers.filter(Address.Country = "US").count()
customers.filter(Address.City = "Seattle").count()22. Insert
products.insert({"ProductID": 9001, "ProductName": "Test Widget", "ListPrice": 99.99, "SubCategoryID": 1})
vendors.insert({"VendorID": 9001, "VendorName": "Test Vendor", "CreditRating": 3, "ActiveFlag": 1})
productcategories.insert({"CategoryID": 99, "CategoryName": "TestCategory"})23. Update (set)
Use .filter().set({fields}), only specified fields are updated:
products.filter(ProductID = 9001).set({"ListPrice": 149.99})
vendors.filter(VendorName = "Test Vendor").set({"CreditRating": 5})
products.filter(MakeFlag = 1 and ListPrice > 100).set({"StandardCost": 75.00})
products.filter(ProductID = 9001).set({"ListPrice": 199.99, "StandardCost": 80.00})24. Delete
products.filter(ProductID = 9001).delete()
vendors.filter(ActiveFlag = 0).delete()
products.filter(MakeFlag = 0 and ListPrice < 5).delete()Operator Reference
Filter Operators
| Operator | Syntax | Description |
|---|---|---|
| Equal | = | Exact match |
| Not Equal | != | Not equal |
| Greater Than | > | Greater than |
| Greater or Equal | >= | Greater than or equal |
| Less Than | < | Less than |
| Less or Equal | <= | Less than or equal |
| In | in [...] | Value in list |
| Between | between A and B | Inclusive range |
| Contains | contains "..." | Substring match |
| Starts With | startsWith "..." | Prefix match |
| Exists | exists true | Field exists check |
| Regex | ~ "pattern" | Regex match |
Logical Operators
| Operator | Syntax |
|---|---|
| AND | and |
| OR | or |
Aggregation Functions
| Function | Syntax |
|---|---|
| Count | aggregate(alias: count) |
| Sum | aggregate(alias: sum(field)) |
| Average | aggregate(alias: avg(field)) |
| Minimum | aggregate(alias: min(field)) |
| Maximum | aggregate(alias: max(field)) |
Query Modifiers
| Modifier | Syntax |
|---|---|
| Limit | .limit(N) |
| Skip | .skip(N) |
| Order By | .orderBy(field, asc/desc) |
| Group By | .groupBy(field) |
| Projection | .pluck(field1, field2) |
| Count Only | .count() |
Query Chaining Order
space.store
.filter(...) // optional filter (and/or)
.orderBy(field, dir) // optional sort (chainable)
.limit(N) // optional limit
.skip(N) // optional offset
.pluck(field1, ...) // optional projection
.count() // count only
.aggregate(...) // aggregation
.groupBy(field) // group by
.insert({...}) // mutation: insert
.set({...}) // mutation: update
.delete() // mutation: deleteSecondary Index Usage
Queries automatically use secondary indexes when available:
- Equality (
=), exact key lookup - Range (
>,>=,<,<=), B+ tree range scan - Between (
between A and B), B+ tree range scan (inclusive) - $in, multi-key lookup
Operators that always require full scan:
contains,startsWith,~(regex)exists!=- Queries with no filter
For the full client-side query API and language details, see the Planck client documentation for your language of choice on https://plancks.io/docs.