Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

LangGraph, persistence, and resume

Define typed graph control flow, configure native checkpoint and memory drivers, and resume the same durable thread safely.

A graph describes a workflow whose steps and branching rules belong to your application. RELKIT registers the declared topology and compiles a native LangGraph while preserving native reducers, parallel scheduling, conditional edges, loops, Send, subgraphs, retry behavior, and pending writes.

Agent graph state and checkpoint resources remain the authority for graph continuation. A task may invoke an agent, but task retries do not silently replace the graph’s thread or checkpoint identity; use the existing agent continuation contract.

Define state and control flow

The commerce review graph runs two branches, joins them, loops once, interrupts for a boolean, and returns only its selected output:

const orderReview = defineGraph({
  state,
  input: z.object({ orderId: z.string() }),
  output: z.object({ result: z.string() }),
  nodes: [inventory, risk, retry, review, finish],
  edges: (edge) =>
    edge
      .addEdge(START, "load-inventory")
      .addEdge(START, "assess-risk")
      .addEdge(["load-inventory", "assess-risk"], "retry")
      .addConditionalEdges("retry", ({ attempts }) => (attempts === 1 ? "again" : "ready"), {
        again: "retry",
        ready: "review",
      })
      .addEdge("finish", END),
  checkpointer: orderCheckpoints,
  store: orderMemory,
  limits: { maxSteps: 10, maxToolCalls: 1, timeoutMs: 20_000 },
  stateProfile: "agents",
  client: {
    public: true,
    state: ["inventory", "risk", "attempts", "approved", "result"],
  },
  controls: ["stop"],
});

Run and resume this graph from the frontend

After frontend setup and pulling the commerce contract, render this component under the provider with your application’s stable business threadId:

"use client";

/** Starts the commerce review graph and answers its boolean interrupt on the same business thread. */
import { useAgent, useRelkitClient } from "@relkit/client/react";
import { useState } from "react";

/** Keeps initial input and a waiting reply distinct while rendering the graph's final result.
 * @param props - Authorized order-review thread retained by the application.
 * @returns Start/approve actions, current result, and visible submission failure. */
export function OrderReview({ threadId }: { readonly threadId: string }) {
  const { status } = useRelkitClient();
  const review = useAgent("orders.order-review");
  const [failure, setFailure] = useState("");

  /** Starts one review; uncertain acceptance is left for explicit thread recovery.
   * @returns Completion after acceptance or displaying the failure. */
  async function startReview() {
    try {
      await review.run({ orderId: "demo-1" }, { threadId });
    } catch (error) {
      setFailure(String(error));
    }
  }

  /** Replies with the review node's declared boolean on the original thread.
   * @returns Completion after the continuation is accepted or its failure is displayed. */
  async function approve() {
    try {
      await review.run(true, { threadId, resume: true });
    } catch (error) {
      setFailure(String(error));
    }
  }

  return (
    <section>
      <button
        disabled={status !== "ready" || review.status !== "idle" || failure !== ""}
        onClick={startReview}
      >
        Start review
      </button>
      <button
        disabled={status !== "ready" || review.status !== "waiting" || failure !== ""}
        onClick={approve}
      >
        Approve order
      </button>
      <p role="status">{review.output?.result ?? review.status}</p>
      {failure && <p role="alert">{failure}. Check this thread before submitting again.</p>}
    </section>
  );
}

defineGraph generates an agent client too. The initial run receives { orderId }; the review node’s resume: z.boolean() makes the continuation accept a boolean. Resume the same thread with { resume: true }, and show the final output.result. Do not send a replacement initial run to answer an interrupt or replay an uncertain submission.

Reference: useAgent. Continue with threads and recovery.

StateSchema owns native state and reducer behavior. A defineGraphNode input schema projects the fields that node can read; its output validates an object partial update. Declare every possible Command destination in ends. The edge builder accepts only registered node IDs plus valid START and END sentinels, so stale destinations fail type checking and compilation.

interrupt() is graph control flow, not a normal function failure. The review node declares resume: z.boolean(), then returns a native Command that applies the validated reply and routes to finish. A resumed node starts again according to LangGraph semantics. Put the interrupt before an irreversible effect, or make that effect idempotent with a durable business key or application-owned transaction.

Declare checkpoint and memory resources

Checkpoint history and long-term memory are separate resources:

/** Shares borrowed in-memory LangGraph checkpoint and memory drivers across the example's order agents. */
import { InMemoryStore, MemorySaver } from "@langchain/langgraph";
import { defineCheckpointerDb, defineMemoryDb } from "@relkit/langchain";

// Replace these borrowed native drivers with SQLite, Postgres, MongoDB, or Redis in production.
export const orderCheckpoints = defineCheckpointerDb({
  id: "orders.agent-checkpoints",
  client: new MemorySaver(),
});

export const orderMemory = defineMemoryDb({
  id: "orders.agent-memory",
  client: new InMemoryStore(),
});

defineCheckpointerDb accepts a native BaseCheckpointSaver; defineMemoryDb accepts a native BaseStore. The tested driver cohort is:

Resource Supported native implementations
Checkpointer MemorySaver, SQLite, PostgreSQL, MongoDB, Redis and shallow Redis, or a compatible custom saver
Memory store InMemoryStore, PostgreSQL, MongoDB, or a compatible custom store

The tested RedisStore does not satisfy the selected LangGraph BaseStore declarations: it lacks the lifecycle methods and operation-specific batch result typing required by defineMemoryDb. Strict Redis memory-store compatibility is therefore not guaranteed in this cohort. This declaration limitation does not establish that Redis memory cannot run through LangGraph’s internal batching wrapper. The Redis and shallow Redis checkpointer entries above remain supported separately.

Driver-specific retention, TTL, serialization, shallow-history, and semantic-search capabilities remain native. RELKIT does not flatten them into options that every driver supposedly supports.

The in-memory saver/store in the example are supplied by the selected LangGraph SDK. External driver packages are application-owned dependencies. Install only the driver you choose, in the backend root; these pins come from the native integration’s tested driver declarations:

Driver Tested package Install only your selected driver
sqlite @langchain/langgraph-checkpoint-sqlite@1.0.4 bun add @langchain/langgraph-checkpoint-sqlite@1.0.4
postgres @langchain/langgraph-checkpoint-postgres@1.0.5 bun add @langchain/langgraph-checkpoint-postgres@1.0.5
mongodb @langchain/langgraph-checkpoint-mongodb@1.4.1 bun add @langchain/langgraph-checkpoint-mongodb@1.4.1
redis @langchain/langgraph-checkpoint-redis@1.0.11 bun add @langchain/langgraph-checkpoint-redis@1.0.11

An installation/type compatibility check does not initialize its database or certify a production driver deployment. Keep the Redis memory-store limitation above when choosing a driver.

Initialize and own resources explicitly

RELKIT never calls driver .setup(), runs migrations, creates tables or indexes, installs extensions, or pushes an application schema during compilation, inspection, activation, or an ordinary request. Perform the driver’s documented initialization as an explicit application or infrastructure step before starting workers.

A supplied instance is borrowed by default and is never closed by RELKIT. A factory is owned by default; owned resources require a dispose callback and are released once. Set ownership explicitly when the default does not match the real lifecycle.

Prefer a dedicated AI database. Sharing a physical database requires a compatible underlying client, isolated tables or schemas, a restricted role, and migration filters that keep checkpoint/store objects out of the application Drizzle lifecycle. A RELKIT Redis cache provider is not a Redis saver or store.

Resume the same business thread

Every initial run, observation, stop, steering operation, follow-up, approval, and continuation supplies a caller-owned threadId. RELKIT never derives one from input, remembers an implicit current thread, or generates a fallback. Internal run, task, attempt, and operation IDs are separate.

After the graph persists its checkpoint and waiting contract, submit the exact declared reply on the same thread with { resume: true }. A boolean wait rejects text such as "yes"; RELKIT does not interpret it. The runtime binds the continuation to the waiting revision, scope, action, policy, and response schema, deduplicates an accepted receipt, and fences competing workers.

See Threads and recovery for restart and external-effect safety, and Agent streaming for the public event sequence.