Core concepts
Memory
Share state across steps and runs with pluggable memory providers.
Agents in a workflow do not share state automatically — each step sees its input and produces its output. Memory is the layer that carries state across steps and, when you choose a durable provider, across runs and processes. It backs both the run's message thread and its artifact store, so the same provider choice controls where everything the run remembers actually lives.
The design is deliberately small: a MemoryProvider is four async functions over JSON values. The SDK ships two providers — in-memory and JSON file — and two typed views built on top of them: thread memory for messages and the artifact store for keyed run outputs.
How a workflow uses memory#
Every run gets a thread memory keyed by its runId and an artifact store scoped to the run, both backed by the workflow's provider. If you set no memory config, the runtime creates a fresh in-memory provider for each run — nothing leaks between runs, and nothing survives the process.
Attach a provider on the workflow definition to change that:
import { createJsonFileMemoryProvider, workflow } from "@assemble-dev/sdk";
import { z } from "zod";
const provider = createJsonFileMemoryProvider({ filePath: ".assemble/memory.json" });
export const supportWorkflow = workflow({
name: "supervised-support",
inputSchema: z.object({ ticket: z.string().min(1) }),
agents: [classifier, refundSpecialist],
memory: { provider },
});memory takes a WorkflowMemoryConfig — currently just { provider }. With a file provider, resumed runs (see human approval) and later processes read the same state the original run wrote.
The MemoryProvider interface#
Any object with these four methods works as a provider, so you can back memory with Redis or Postgres without touching the SDK:
type MemoryProvider = {
getMemoryValue(key: string): Promise<JsonValue | null>;
setMemoryValue(key: string, value: JsonValue): Promise<void>;
appendMemoryValue(key: string, value: JsonValue): Promise<void>;
listMemoryKeys(): Promise<string[]>;
};appendMemoryValue treats the stored value as a list: appending to a missing key creates a one-element array, appending to an existing array pushes onto it, and appending to a scalar wraps both values into an array. Thread memory is built entirely on this behavior.
createInMemoryMemoryProvider#
An ephemeral provider backed by a Map. Values are structured-cloned on both write and read, so callers cannot mutate stored state through shared references.
import { createInMemoryMemoryProvider } from "@assemble-dev/sdk";
const provider = createInMemoryMemoryProvider();
await provider.setMemoryValue("customer:ord_1", { tier: "pro" });
console.log(await provider.getMemoryValue("customer:ord_1"));This is what the runtime uses by default. Reach for it explicitly in tests, or when several workflows in one process should share a single ephemeral store.
createJsonFileMemoryProvider#
A durable provider that persists every write to one JSON file. It creates parent directories as needed, loads the file lazily on first access, and tolerates a missing or corrupt file by starting empty.
import { createJsonFileMemoryProvider } from "@assemble-dev/sdk";
const provider = createJsonFileMemoryProvider({ filePath: ".assemble/memory.json" });| Option | Type | Description |
|---|---|---|
filePath | string | Path to the JSON file. Created (with parent directories) on first write. |
Note: The file provider rewrites the whole file on every write and caches entries in process memory. It is built for local development and single-process apps, not for concurrent writers.
Thread memory#
createThreadMemory({ provider, threadKey }) gives you a typed message log on top of any provider. Messages are stored under thread:<threadKey>:messages; the runtime uses the runId as the thread key, and you can use your own key — a user id, a conversation id — to build threads that span runs.
import { createJsonFileMemoryProvider, createThreadMemory } from "@assemble-dev/sdk";
const provider = createJsonFileMemoryProvider({ filePath: ".assemble/memory.json" });
const thread = createThreadMemory({ provider, threadKey: "run_1" });
await thread.appendThreadMessage({
role: "user",
content: "remember the order id ord_1",
timestamp: new Date().toISOString(),
});
const messages = await thread.listThreadMessages();appendThreadMessage takes a RunMessage: role ("system", "user", "assistant", or "tool"), content, an ISO timestamp, and optional agentName and toolName. listThreadMessages validates what it reads with Zod and returns RunMessage[], so a corrupted store fails loudly rather than feeding malformed messages to your agents.
Artifact store#
createArtifactStore({ provider, runId }) is a keyed store for a run's artifacts — the same RunArtifact values that surface in RunState.artifacts (see runtime). Artifacts live under a single memory key, run:<runId>:artifacts, as a record keyed by artifact key, so saving with an existing key overwrites that artifact.
import { createArtifactStore, createInMemoryMemoryProvider } from "@assemble-dev/sdk";
const store = createArtifactStore({ provider: createInMemoryMemoryProvider(), runId: "run_1" });
await store.saveRunArtifact({
key: "classification",
value: { topic: "refund", urgency: "high" },
producedByAgentName: "classifier",
createdAt: new Date().toISOString(),
});
const artifact = await store.getRunArtifact("classification");
const allArtifacts = await store.listRunArtifacts();The runtime writes its own artifacts through this store — for example the parallel:merged output of a parallel topology (see workflows) and the pending-approval record that lets a paused run resume. getRunArtifact returns null for a missing key, and all reads are Zod-validated.
Choosing a provider#
| Provider | Survives the process? | Shared across runs? | Best for |
|---|---|---|---|
| Default (none configured) | No | No — fresh provider per run | Isolated runs, evals, quick starts. |
createInMemoryMemoryProvider() | No | Yes, within one process | Tests, short-lived multi-run scripts. |
createJsonFileMemoryProvider({ filePath }) | Yes | Yes | Local development, resumable approval flows, threads spanning runs. |
Custom MemoryProvider | Up to you | Up to you | Production stores like Redis or Postgres. |
The practical rule: leave memory unset until a step needs to see something an earlier run wrote. Then start with the file provider, and swap in a custom provider when you outgrow a single process — the four-method interface is the only contract.
Where to go next#
- See how artifacts and messages land in
RunStatein runtime. - Persisted run state for pause and resume is separate from memory — see human approval.
- Full workflow configuration lives in workflows.