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.

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.

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_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 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

Last updated on