Schemas
Schemas are the foundation of structured memory in zkStash. They define what your agents should remember and how that knowledge is organized.
What Are Schemas?
Think of schemas as blueprints for your agent’s knowledge. Just like a database table has columns with specific types, a schema defines the structure of memories your agent stores.
Without schemas, memory is just a “dump” of unstructured text:
"User likes dark mode and speaks English"With schemas, the same information becomes queryable, type-safe, and consistent:
{
"theme": "dark",
"language": "en"
}Why Use Schemas?
Type Safety
Ensure that a “date” is actually a date, not a string. A “score” is a number, not text. This prevents bugs and makes your agent’s knowledge reliable.
Consistency
All agents (or all instances of the same agent) store UserProfile data in the exact same way. No more guessing which field holds the user’s name.
Queryability
Filter and search memories programmatically:
- Find all tasks where
status == "urgent" - Retrieve memories created after a specific timestamp
- Count how many interactions had
sentiment == "positive"
LLM-Friendly
Schemas provide clear instructions to the LLM about what data to extract and how to structure it. This significantly improves extraction accuracy.
Schema Types: Single vs Multiple
Single Record (Profiles)
One version of truth per context. Perfect for entities where only the latest state matters.
Use Cases:
- User preferences (
UserProfile) - Agent configuration (
AgentSettings) - Current status (
ProjectStatus)
Behavior: When you create a new memory with a “single” schema for the same context (Agent + Thread), it replaces the previous one.
{
"name": "UserProfile",
"cardinality": "single",
"schema": {
"type": "object",
"properties": {
"theme": { "type": "string", "enum": ["light", "dark"] },
"language": { "type": "string" }
},
"required": ["theme"]
}
}Multiple Records (Collections)
Accumulate knowledge over time. Perfect for events, logs, or growing lists of facts.
Use Cases:
- Interaction history (
ConversationLog) - Task tracking (
TaskHistory) - Knowledge base (
ProductFact)
Behavior: Each new memory is appended. You can have thousands of records for the same schema.
{
"name": "TaskHistory",
"cardinality": "multiple",
"schema": {
"type": "object",
"properties": {
"taskId": { "type": "string" },
"status": { "type": "string", "enum": ["completed", "failed"] },
"timestamp": { "type": "number" }
},
"required": ["taskId", "status"]
}
}Creating Schemas
Schemas are defined using JSON Schema , a widely-adopted standard for describing JSON data structures. For TypeScript developers, we strongly recommend using Zod for type-safe schema definitions.
Using Zod (Recommended for TypeScript)
TypeScript (Zod)
import { z } from "zod";
import { fromPrivateKey } from "@zkstash/sdk";
// Authenticate with wallet
const authPrivateKey = process.env.AUTH_PRIVATE_KEY;
const client = await fromPrivateKey(
"solana-devnet",
authPrivateKey
)
// Define schema using Zod
const ShoppingListSchema = z.object({
item: z.string().describe("The name of the item to buy"),
quantity: z.number().int().positive().describe("How many to buy"),
purchased: z.boolean().default(false)
});
// Create schema on zkStash
await client.schemas.create({
name: "ShoppingList",
description: "Items to buy",
cardinality: "multiple",
schema: ShoppingListSchema
});Using Schemas
Once created, schemas become tools for your agent to use.
With MCP
The MCP server automatically generates tools based on your schemas. If you create a UserProfile schema, you get:
create_user_profiletool- Tools are dynamically updated when schemas change
With SDK
Use the schema name when creating memories:
await client.memories.create({
agentId: "my-agent",
schemaName: "ShoppingList",
data: {
item: "Milk",
quantity: 2,
purchased: false
}
});Validation
zkStash validates all memories against their schemas. If data doesn’t match the schema, the request fails. This ensures data integrity.
Supported Field Types
| Type | Description | Example |
|---|---|---|
string | Text data | "dark", "en-US" |
number | Numeric data (int or float) | 42, 3.14 |
boolean | True/False | true, false |
array | List of values | ["tag1", "tag2"] |
object | Nested structure | { "nested": "value" } |
enum | Restricted set of values | ["light", "dark"] |
Advanced Features
- Nested objects: Define complex structures with
objecttype - Arrays of objects:
"type": "array", "items": { "type": "object", ... } - Enums: Restrict values with
"enum": ["option1", "option2"] - Required fields: Mark fields as mandatory with
"required": ["field1"] - Defaults: Set default values with
"default": value - Descriptions: Add
"description"to help LLMs understand fields
Best Practices
1. Keep Schemas Focused
Each schema should represent one concept. Don’t create a mega-schema with unrelated fields.
Good:
UserProfile(theme, language)TaskHistory(taskId, status)
Bad:
EverythingAboutTheUser(theme, language, tasks, shopping list, …)
2. Use Descriptive Names
Schema and field names should be self-explanatory.
Good: TaskHistory, ConversationLog
Bad: Data1, Stuff
3. Add Descriptions
Especially when using MCP, descriptions help the LLM understand when and how to use the schema.
const TaskSchema = z.object({
title: z.string().describe("A brief, actionable title for the task"),
priority: z.enum(["low", "medium", "high"]).describe("Task urgency level")
});4. Choose Cardinality Wisely
- Single: When you only care about the latest state
- Multiple: When you need history or a growing collection
5. Use Enums for Restricted Values
This prevents typos and ensures consistency:
{
"status": { "type": "string", "enum": ["pending", "completed", "failed"] }
}External Resources
- JSON Schema Documentation : Complete reference for JSON Schema
- Zod Documentation : TypeScript-first schema validation
- OpenAI Structured Outputs : Supported schema features and best practices