Recipe, LangGraph + Shelvia
Shelvia is the memory layer LangGraph calls. LangGraph runs the graph; Shelvia returns reviewed context and accepts new memory only through the review queue.
Principle
LangGraph's node-state pattern fits Shelvia exactly: add a retrieval node that runs before the LLM call to fetch a reviewed context pack, then add a finalization node that proposes a candidate when the run produces a new claim worth keeping.
Setup
- Create a Workspace API token in Settings > API with scopes: memory.read + memory.context (read-only graph) OR + memory.candidates (graph that proposes memory).
- Store the token in SHELVIA_API_KEY. Never log it.
- Add @shelvia/sdk to your LangGraph project dependencies.
Pattern, retrieval node
A LangGraph node that fetches a context pack before the LLM call. The pack rides on the graph state for downstream nodes.
import { StateGraph, START, END } from "@langchain/langgraph";
import { ShelviaClient } from "@shelvia/sdk";
const shelvia = new ShelviaClient({ apiKey: process.env.SHELVIA_API_KEY! });
interface RunState {
project_id: string;
purpose: string;
context_pack?: unknown;
agent_output?: string;
}
const graph = new StateGraph<RunState>({
channels: { project_id: null, purpose: null, context_pack: null, agent_output: null },
});
graph.addNode("retrieve_shelvia_context", async (state) => {
const pack = await shelvia.context.generatePack(state.project_id, {
purpose: state.purpose,
max_chars: 12000,
semantic_ranking: true,
});
return { context_pack: pack };
});
graph.addNode("run_llm", async (state) => {
// call your LLM here with state.context_pack as the system context
return { agent_output: "..." };
});
graph.addEdge(START, "retrieve_shelvia_context");
graph.addEdge("retrieve_shelvia_context", "run_llm");
graph.addEdge("run_llm", END);
const app = graph.compile();Pattern, finalization node proposes a candidate
After the LLM produces an output worth remembering, a finalization node proposes it as a Shelvia memory candidate. The row lands in the review queue with status pending.
graph.addNode("propose_candidate", async (state) => {
await shelvia.memory.createCandidate(
state.project_id,
{
type: "decision",
title: "LangGraph run decided: " + state.purpose,
content: state.agent_output ?? "",
source_url: null,
},
{
idempotencyKey: `agent-langgraph-${state.run_id}-decision-${hash(state.agent_output)}`,
},
);
return {};
});
graph.addEdge("run_llm", "propose_candidate");
graph.addEdge("propose_candidate", END);Propose memory candidates safely
LangGraph nodes that propose Shelvia memory candidates must follow the candidate-write safety pattern. Agents propose. Humans approve. The candidate enters the review queue; it does not become trusted memory until a workspace member approves.
- Identity: create a narrow-scope Workspace API token per LangGraph workflow (label it, e.g., "LangGraph research workflow"). Reuse shv_live_<hex> with memory.candidates + memory.context.
- Idempotency: every candidate write should carry an Idempotency-Key shaped agent-langgraph-<run-id>-<candidate-hash>. LangGraph nodes may re-execute on retry; idempotency keeps the review queue clean.
- Runtime tag: emit shelvia.projects.logContinuation({ suggested_tool: "langgraph", ... }) as the final node. The candidate body itself does not carry a runtime field.
- Never write to trusted memory directly. There is no shelvia.projects.writeDecision(). Use shelvia.memory.createCandidate(...) only.
- Never log raw chain-of-thought. Keep candidate content to the final claim; the optional `reasoning` field carries a short justification, not the full reasoning trace.
- Never include secrets or tokens. The validator rejects forbidden keys (cookie, authorization, session_token, api_key, ...) with a structured error code.
- Agents must not approve their own memory. Approval is a UI action in the Shelvia review surface, performed by a workspace member.
Observability (optional)
A LangGraph node can log a continuation after the graph completes. The event records which reviewed context was used and what should happen next. This is an optional audit / observability emit, it does NOT become trusted memory and does NOT bypass review-before-save. Scope required: continuations.log.
graph.addNode("log_continuation", async (state) => {
await shelvia.projects.logContinuation(state.project_id, {
next_best_action: state.purpose,
why: "LangGraph run completed using the Shelvia context pack.",
suggested_tool: "langgraph",
priority: "normal",
});
return {};
});
graph.addEdge("propose_candidate", "log_continuation");
graph.addEdge("log_continuation", END);Safety notes
- Shelvia is not the LangGraph runtime, LangGraph is. Don't model Shelvia as a checkpoint store; use LangGraph's own checkpoint surface for durability.
- Every proposed candidate is pending. A workspace member must approve before it becomes trusted memory or appears in future context packs.
- The continuation log is audit-only. It does not write to trusted memory tables.
- Use the Idempotency-Key header on candidate writes if the graph might re-execute the finalization node.
Where to go next
- TypeScript SDK reference, /docs/developers/sdk
- REST API reference, /docs/developers/api
- Context Packs concept, /docs/concepts/context-packs
For runnable code samples and the developer reference, see /developers. For the trust model in depth, see /security.