Skip to Content

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.

Auto-Supersede with uniqueOn

The uniqueOn field defines which fields identify the same logical entity. When a new memory matches an existing one on these fields, it automatically supersedes (replaces) the old version.

Profile Pattern (One Per User)

Use uniqueOn: ["kind"] when you want one version of truth per schema type. Perfect for entities where only the latest state matters.

Use Cases:

  • User preferences (UserProfile)
  • Agent configuration (AgentSettings)
  • Current status (ProjectStatus)

Behavior: New memories automatically replace previous ones of the same kind.

{ "name": "UserProfile", "uniqueOn": ["kind"], "schema": { "type": "object", "properties": { "theme": { "type": "string", "enum": ["light", "dark"] }, "language": { "type": "string" } }, "required": ["theme"] } }

Collection Pattern (Accumulating Records)

Omit uniqueOn when you want to 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", "schema": { "type": "object", "properties": { "taskId": { "type": "string" }, "status": { "type": "string", "enum": ["completed", "failed"] }, "timestamp": { "type": "number" } }, "required": ["taskId", "status"] } }

Custom Unique Keys

You can also specify custom fields for uniqueness. For example, uniqueOn: ["email"] ensures only one contact per email address:

{ "name": "Contact", "uniqueOn": ["email"], "schema": { "type": "object", "properties": { "email": { "type": "string" }, "name": { "type": "string" }, "company": { "type": "string" } }, "required": ["email", "name"] } }

When you store a contact with email: "alice@example.com" twice, the second one supersedes the first.

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.

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 (no uniqueOn = accumulating list) await client.registerSchema({ name: "ShoppingList", description: "Items to buy", 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_profile tool
  • 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

TypeDescriptionExample
stringText data"dark", "en-US"
numberNumeric data (int or float)42, 3.14
booleanTrue/Falsetrue, false
arrayList of values["tag1", "tag2"]
objectNested structure{ "nested": "value" }
enumRestricted set of values["light", "dark"]

Advanced Features

  • Nested objects: Define complex structures with object type
  • 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 uniqueOn Wisely

  • uniqueOn: ["kind"]: When you only care about the latest state (e.g., profiles)
  • uniqueOn: ["email"]: When you want one record per unique field value
  • No uniqueOn: 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

Last updated on