Crux
GuidesConvex

Storage, Memory, Workspaces

Use Convex Storage Beta adapters, memory blocks, blackboards, and Convex file storage from Convex functions.

Your agent's memory blocks, blackboards, plans, flow state, and workspace files need somewhere durable to live. If your app runs on Convex, convexRecordStore() keeps JSON records in your Convex deployment. convexStorage() bundles those records and includes file assets only when you pass an explicit assets configuration. It does not provide vector search.

Shared Records

Define one helper and reuse it everywhere a Crux primitive asks for records:

convex/crux/storage.ts
import type { ActionCtx, MutationCtx } from "../_generated/server";
import { components } from "../_generated/api";
import { convexRecordStore } from "@use-crux/convex";

export function cruxRecords(ctx: ActionCtx | MutationCtx) {
  return convexRecordStore({ component: components.crux, ctx });
}

Every example below passes cruxRecords(ctx) where a store is required. For the underlying document encoding, TTL and filter semantics, the component query boundary, and the in-memory test component (createInMemoryConvexStoreDocumentComponent()), see the @use-crux/convex reference.

Memory Blocks

Memory blocks work unchanged on Convex. Give them the durable store and a namespace:

convex/agent/memory.ts
import { episodes, facts, memory, workingState } from "@use-crux/convex/memory";
import { z } from "zod";
import { cruxRecords } from "../crux/storage";
import type { ActionCtx } from "../_generated/server";

const State = z.object({
  goal: z.string().optional(),
  draftId: z.string().optional(),
});

export function createAssistantMemory(ctx: ActionCtx, threadId: string) {
  return memory({
    id: "assistant",
    records: cruxRecords(ctx),
    namespace: `thread:${threadId}`,
    blocks: [
      workingState({ id: "state", schema: State }),
      facts({ id: "facts", embed }),
      episodes({ id: "episodes", embed }),
    ],
  });
}

Without a VectorStore, blocks configured with embed still persist embeddings in their records, but semantic recall falls back to the block's recency/list behavior. Pass an explicit vector backend when you need similarity-ranked recall. For example, pair Convex records with Upstash Vector:

convex/agent/memory.ts
import { memory, episodes } from "@use-crux/convex/memory";
import { upstashVectorStore } from "@use-crux/upstash";

const vectors = upstashVectorStore({
  index: vectorIndex,
  namespace: `memory:${threadId}`,
});

const assistantMemory = memory({
  id: "assistant",
  records: cruxRecords(ctx),
  vectors,
  namespace: `thread:${threadId}`,
  blocks: [episodes({ id: "episodes", embed })],
});

From action/runtime code, the standalone episode API takes records, namespace, and an optional memoryId—there is no store option:

await episodesBlock.record(
  { content: "User asked about pricing" },
  {
    records: cruxRecords(ctx),
    namespace: `thread:${threadId}`,
    memoryId: "assistant",
  },
);

If a mutation needs to write application-specific memory directly, keep that helper in your app so table names, authorization, and tenancy remain explicit.

Blackboards

Blackboards are core primitives. Use the Convex store and namespace them by thread, workflow, or task.

convex/agent/blackboard.ts
import { blackboard } from "@use-crux/core/agent";
import { z } from "zod";
import { cruxRecords } from "../crux/storage";
import type { ActionCtx } from "../_generated/server";

export function createResearchBoard(ctx: ActionCtx, threadId: string) {
  return blackboard({
    id: "research",
    records: cruxRecords(ctx),
    namespace: `thread:${threadId}`,
    schema: z.object({
      queries: z.array(z.string()).default([]),
      synthesis: z.string().optional(),
    }),
  });
}

When a prompt resolves blackboard tools and you pass them to Convex Agent, bridge them with convexTools() from @use-crux/convex/agent.

Workspaces

Use convexAssetStore() for binary and oversized workspace payloads. Keep metadata in the Convex record store.

convex/workspace.ts
import { storage } from "@use-crux/core/storage";
import { workspace } from "@use-crux/core/workspace";
import { convexRecordStore, convexAssetStore } from "@use-crux/convex";
import { components } from "./_generated/api";
import type { ActionCtx } from "./_generated/server";

export function createThreadWorkspace(ctx: ActionCtx, threadId: string) {
  return workspace({
    id: "thread-workspace",
    namespace: threadId,
    storage: storage({
      records: convexRecordStore({ component: components.crux, ctx }),
      assets: convexAssetStore({ ctx }),
    }),
  });
}

Convex file storage methods differ by function context. If asset reads are unavailable, convexAssetStore().get() throws clearly.

Semantic Cache

Convex bundled storage supplies records, not vectors. Pair it with an explicit pre-filter-capable VectorStore for semantic cache lookup, and isolate that vector namespace from RAG chunks and memory entries.

import { createSemanticCache } from "@use-crux/core/cache";
import { convexRecordStore } from "@use-crux/convex";
import { upstashVectorStore } from "@use-crux/upstash";

const cache = createSemanticCache({
  records: convexRecordStore({ component: components.crux, ctx }),
  vectors: upstashVectorStore({
    index: vectorIndex,
    namespace: "semantic-cache",
  }),
  embedding,
  ttl: 60_000,
});

Best Practices

  • Namespace by thread, project, task, or workflow for key scoping and inspection. Namespaces do not authorize access; enforce tenant access in your Convex functions.
  • Use convexRecordStore() for JSON records. Configure convexAssetStore() explicitly for file bytes.
  • Keep mutation-only direct table writes app-local.
  • Pass an explicit VectorStore for semantic recall and use a dedicated vector space for semantic cache.
  • Do not create separate Convex-specific memory, blackboard, or workspace primitives unless Convex infrastructure requires a new lifecycle.

On this page