Shelvia
RecipeAvailable

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.