Agent skill
workspace-api
Workspace API patterns for defineTable, defineKv, versioning, migrations, and data access (CRUD + observation). Use when the user mentions workspace, defineTable, defineKv, createWorkspace, or when defining schemas, reading/writing table data, observing changes, or writing migrations.
Install this agent skill to your Project
npx add-skill https://github.com/EpicenterHQ/epicenter/tree/main/.agents/skills/workspace-api
Metadata
Additional technical details for this skill
- author
- epicenter
- version
- 5.0
SKILL.md
Workspace API
Reference Repositories
- Yjs — CRDT framework (foundation of workspace data layer)
Type-safe schema definitions for tables and KV stores.
Related Skills: See
yjsfor Yjs CRDT patterns and shared types. Seesveltefor reactive wrappers (fromTable,fromKv).
When to Apply This Skill
- Defining a new table or KV store with
defineTable()ordefineKv() - Adding a new version to an existing table definition
- Writing table migration functions
- Reading, writing, or observing table/KV data
Tables
Shorthand (Single Version)
Use when a table has only one version:
import { defineTable } from '@epicenter/workspace';
import { type } from 'arktype';
const usersTable = defineTable(type({ id: UserId, email: 'string', _v: '1' }));
export type User = InferTableRow<typeof usersTable>;
Every table schema must include _v with a number literal. The type system enforces this — passing a schema without _v to defineTable() is a compile error.
Builder (Multiple Versions)
Use when you need to evolve a schema over time:
const posts = defineTable()
.version(type({ id: 'string', title: 'string', _v: '1' }))
.version(type({ id: 'string', title: 'string', views: 'number', _v: '2' }))
.migrate((row) => {
switch (row._v) {
case 1:
return { ...row, views: 0, _v: 2 };
case 2:
return row;
}
});
KV Stores
KV stores use defineKv(schema, defaultValue). No versioning, no migration—invalid stored data falls back to the default.
import { defineKv } from '@epicenter/workspace';
import { type } from 'arktype';
const sidebar = defineKv(type({ collapsed: 'boolean', width: 'number' }), { collapsed: false, width: 300 });
const fontSize = defineKv(type('number'), 14);
const enabled = defineKv(type('boolean'), true);
KV Design Convention: One Scalar Per Key
Use dot-namespaced keys for logical groupings of scalar values:
// ✅ Correct — each preference is an independent scalar
'theme.mode': defineKv(type("'light' | 'dark' | 'system'"), 'light'),
'theme.fontSize': defineKv(type('number'), 14),
// ❌ Wrong — structured object invites migration needs
'theme': defineKv(type({ mode: "'light' | 'dark'", fontSize: 'number' }), { mode: 'light', fontSize: 14 }),
With scalar values, schema changes either don't break validation (widening 'light' | 'dark' to 'light' | 'dark' | 'system' still validates old data) or the default fallback is acceptable (resetting a toggle takes one click).
Exception: discriminated unions and Record<string, T> | null are acceptable when they represent a single atomic value.
Branded Table IDs (Required)
Every table's id field and every string foreign key field MUST use a branded type instead of plain 'string'. This prevents accidental mixing of IDs from different tables at compile time.
Pattern
Define a branded type + arktype validator + generator in the same file as the workspace definition:
import type { Brand } from 'wellcrafted/brand';
import { type } from 'arktype';
import { generateId, type Id } from '@epicenter/workspace';
// 1. Branded type + arktype validator (co-located with workspace definition)
export type ConversationId = Id & Brand<'ConversationId'>;
export const ConversationId = type('string').as<ConversationId>();
// 2. Generator function — the ONLY place with the cast
export const generateConversationId = (): ConversationId =>
generateId() as ConversationId;
// 3. Use in defineTable + co-locate type export
const conversationsTable = defineTable(
type({
id: ConversationId, // Primary key — branded
title: 'string',
'parentId?': ConversationId.or('undefined'), // Self-referencing FK
_v: '1',
}),
);
export type Conversation = InferTableRow<typeof conversationsTable>;
// 4. At call sites — use the generator, never cast directly
const newId = generateConversationId(); // Good
// const newId = generateId() as string as ConversationId; // Bad
Workspace File Structure
A workspace file has two layers:
- Table definitions with co-located types —
defineTable(schema)as standalone consts, each immediately followed byexport type = InferTableRow<typeof table> createWorkspace(defineWorkspace({...}))call — composes pre-built tables into the client
The _v Convention
_vis a number discriminant field ('1'in arktype = the literal number1)- Required for tables — enforced at the type level via
CombinedStandardSchema<{ id: string; _v: number }> - Not used by KV stores — KV has no versioning;
defineKv(schema, defaultValue)is the only pattern - In arktype schemas:
_v: '1',_v: '2',_v: '3'(number literals) - In migration returns:
_v: 2(TypeScript narrows automatically,as constis unnecessary) - Convention:
_vgoes last in the object ({ id, ...fields, _v: '1' })
References
Load these on demand based on what you're working on:
- If working with table migrations (migration function rules, direct-to-latest strategy, migration anti-patterns,
as constnote), read references/table-migrations.md - If working with table/KV CRUD or observation (
get,set,update,observe, Svelte observer guidance), read references/table-kv-crud-observation.md - If working with document content APIs (
withDocument,handle.read/write, mode bindings,handle.batch,handle.ydocanti-pattern), read references/document-content.md
Code references:
packages/workspace/src/workspace/define-table.tspackages/workspace/src/workspace/define-kv.tspackages/workspace/src/workspace/index.tspackages/workspace/src/workspace/create-tables.tspackages/workspace/src/workspace/create-kv.ts
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
svelte
Svelte 5 patterns including runes ($state, $derived, $props), TanStack Query, SvelteMap reactive state, shadcn-svelte components, and component composition. Use when the user mentions .svelte files, Svelte components, or when using TanStack Query, fromTable/fromKv, or shadcn-svelte UI.
autumn
Integrate Autumn billing—define features/plans in autumn.config.ts, use autumn-js SDK for credit checks/tracking, manage the atmn CLI for push/pull. Use when working on billing, pricing, credits, plan gating, or metered usage.
handoff-prompt
Draft a self-contained implementation prompt that an agent can execute with zero prior context. Use when the user says "draft a prompt", "write a handoff", "make a prompt I can copy-paste", "create a delegation brief", or wants to hand off a task to another agent, tool, or conversation.
typebox
TypeBox and TypeMap patterns for runtime schema validation and JSON Schema generation. Use when the user mentions TypeBox, TypeMap, Standard Schema, or when working with runtime type validation, JSON Schema, or schema-based validation.
factory-function-composition
Apply factory function patterns to compose clients and services with proper separation of concerns. Use when creating functions that depend on external clients, wrapping resources with domain-specific methods, or refactoring code that mixes client/service/method options together.
progress-summary
This skill should be used when the user asks questions like "can you summarize", "what happened", "what did we do", "what's the situation", "where are we at", "explain what's going on", "give me an overview", "what's been done", "tell me about this", "walk me through what happened", or any question asking to understand the current state of work or changes. Provides conversational, PR-style summaries with visual diagrams.
Didn't find tool you were looking for?