claude/plugins/surrealdb/skills/surrealdb/SKILL.md
Use whenever working with SurrealDB — writing queries, defining schemas, configuring indexes, debugging errors, handling record IDs, using the Go SDK, or discussing SurrealDB architecture. Activate on any mention of SurrealDB, SurrealQL, HNSW indexes, or surreal-related Go SDK code.
npx skillsauth add raphi011/skills surrealdbInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick-reference for SurrealDB v3.0 patterns, gotchas, and SDK usage. Updated for v3.0.2.
For complete details: references/full-reference.md
These bite silently — check every time.
UPSERT ... WHERE only updates existing rows — never creates new records when WHERE matches nothing. Returns empty result silently.
-- BAD: silently does nothing if no record matches
UPSERT asset SET data = $data WHERE vault = $v AND path = $p
-- GOOD: check-then-create/update pattern
-- 1. SELECT to check existence
-- 2. If not found: CREATE
-- 3. If found: UPDATE
-- 4. On unique constraint violation during CREATE: retry as UPDATE (race)
Even on option<array<float>> fields, setting embedding = NONE causes: Expected 'array' but found 'NONE'.
Fix: Omit the field entirely from CREATE. UPDATE later when embedding is available.
-- BAD
CREATE chunk SET content = $content, embedding = NONE
-- GOOD
CREATE chunk SET content = $content
-- later: UPDATE chunk:xyz SET embedding = $embedding
SurrealDB's bytes type does not accept NULL. Go nil slice → Expected 'bytes' but found 'NULL'.
Fix: Normalize nil to []byte{} before passing to the query.
type::record("table", $id) — if $id already has a table prefix (e.g., "vault:default"), result is vault:vault:default. Silently matches nothing.
Fix: Strip table prefix with bareID helper:
func bareID(table, id string) string {
return strings.TrimPrefix(id, table+":")
}
id INSIDE $ids — passing []string{"document:abc123"} silently matches nothing due to CBOR type distinction.
Fix: Pass []surrealmodels.RecordID:
recordIDs := make([]surrealmodels.RecordID, len(ids))
for i, id := range ids {
recordIDs[i] = newRecordID("document", bareID("document", id))
}
Parenthesized OR in WHERE clauses can cause parse errors (v3.0.2):
-- FAILS
DELETE FROM folder WHERE vault = $v AND (path = $p OR string::starts_with(path, $prefix))
-- WORKAROUND: split into separate queries (use transaction if atomicity needed)
Cannot use type::record() inline in RELATE endpoints — parse error.
-- FAILS
RELATE type::record("doc", $from)->rel->type::record("doc", $to)
-- WORKS
LET $from = type::record("doc", $from_id);
LET $to = type::record("doc", $to_id);
RELATE $from->rel->$to SET ...
Note: multi-statement queries return one result per statement. With 2 LETs + RELATE, the RELATE result is at index [2].
Cannot appear in the same query in v3.0 (was allowed in v2).
-- FAILS
SELECT labels, count() FROM document SPLIT labels GROUP BY labels
-- WORKS: subquery
SELECT label, count() AS count
FROM (SELECT labels AS label FROM document SPLIT labels)
GROUP BY label ORDER BY count DESC
[-1] returns NONE. Use array::last() or array::at() instead.
Standalone computed expressions (no table) need RETURN, not SELECT:
-- FAILS: Unexpected end of file, expected FROM
SELECT array::distinct(array::flatten(...)) AS labels
-- WORKS
RETURN array::distinct(array::flatten(...))
Two forms depending on index presence:
HNSW form <|K,EF|> (requires HNSW index):
WHERE doc.vault = $v AND embedding <|10,40|> $vecBrute force <|K,DIST|> (no index):
embedding <|10,40|> $query_vec -- HNSW
embedding <|10,COSINE|> $query_vec -- brute force
-- Strict mode (per-database, not server-wide in v3.0)
DEFINE DATABASE IF NOT EXISTS mydb STRICT;
-- HNSW vector index
DEFINE INDEX idx_embedding ON chunk FIELDS embedding
HNSW DIMENSION 1024 DIST COSINE TYPE F32 EFC 150 M 12 HASHED_VECTOR;
-- Fulltext analyzer + index
DEFINE ANALYZER my_analyzer TOKENIZERS class FILTERS lowercase, ascii, snowball(english);
DEFINE INDEX idx_ft ON chunk FIELDS content FULLTEXT ANALYZER my_analyzer BM25;
-- Async events with bounded retries
DEFINE EVENT cascade ON document WHEN $event = "DELETE" ASYNC RETRY 3 THEN {
DELETE FROM chunk WHERE document = $before.id
};
tx, err := db.Begin(ctx)
if err != nil { return err }
defer tx.Cancel(ctx)
_, err = surrealdb.Query[any](ctx, tx, "DELETE FROM doc WHERE vault = $v", vars)
if err != nil { return err }
err = tx.Commit(ctx)
tx satisfies same sendable interface as db — pass to surrealdb.Query[T]() directly| Type | When | Key Fields |
|------|------|------------|
| RPCError | Transport failures | Code, Message |
| QueryError | surrealdb.Query() failures | Message only |
| ServerError | RPC method failures (Create, Insert, etc.) | Kind, Details, Cause |
Same DB error surfaces as different Go types depending on call path (Query vs RPC method).
rows := make([]map[string]any, len(items))
for i, item := range items {
rows[i] = map[string]any{"field": item.Field}
}
surrealdb.Query(ctx, db, `INSERT INTO table $rows ON DUPLICATE KEY UPDATE id = id`, map[string]any{"rows": rows})
For record<T> fields: pass typed surrealmodels.RecordID values, not strings.
rews (reconnecting websocket) for productionsurrealcbor) for proper type handlingsurrealdb.New() / surrealdb.Connect() deprecated → use surrealdb.FromEndpointURLString() or surrealdb.FromConnection()type::thing() → type::record(), rand::guid() → rand::id()DEFINE TOKEN / DEFINE SCOPE removedMTREE index removed (use HNSW)LET keyword now mandatory.id idiom → .id() functiondevelopment
Create polished, professional reveal.js presentations. Use when the user asks to create slides, a presentation, a deck, or a slideshow. Supports themes, multi-column layouts, code highlighting, animations, speaker notes, and custom styling. Generates HTML + CSS with no build step required.
tools
Use when writing Tailwind CSS v4 code, configuring Tailwind v4 with @theme or @variant directives, migrating from Tailwind v3 to v4, setting up CSS-native config (no tailwind.config.js), defining semantic color tokens, implementing dark mode with class-based @variant, creating design system tokens, or styling components with utility classes. Covers @import "tailwindcss", @theme blocks, @variant, @layer, CSS custom properties for colors, and common layout/component patterns.
development
Use whenever working with SurrealDB — writing queries, defining schemas, configuring indexes, debugging errors, handling record IDs, using the Go SDK, or discussing SurrealDB architecture. Activate on any mention of SurrealDB, SurrealQL, HNSW indexes, or surreal-related Go SDK code.
development
Use when visually verifying terminal UI rendering, testing TUI interactions, debugging Bubbletea display issues, or when asked to "test the TUI", "screenshot the terminal", "check what the TUI looks like", or "visually verify". Requires Kitty terminal with allow_remote_control and macOS for screencapture.