LangGraph Tutorial (TypeScript): handling long documents for intermediate developers

By Cyprian AaronsUpdated 2026-04-22
langgraphhandling-long-documents-for-intermediate-developerstypescript

This tutorial shows how to build a LangGraph workflow in TypeScript that can ingest long documents, split them into chunks, process each chunk, and combine the results into one answer. You need this when a single prompt blows past model context limits or when you want a more reliable pipeline for contract review, policy analysis, or long-form document QA.

What You'll Need

  • Node.js 18+
  • A TypeScript project
  • @langchain/langgraph
  • @langchain/openai
  • @langchain/core
  • An OpenAI API key in OPENAI_API_KEY
  • A long text file or document content to test with

Install the packages:

npm install @langchain/langgraph @langchain/openai @langchain/core
npm install -D typescript tsx @types/node

Step-by-Step

  1. Start by defining a graph state that carries the document text, chunk list, current chunk index, partial summaries, and final output. For long documents, the key is not “one prompt with everything”; it is “split, process, merge.”
import { Annotation, START, END, StateGraph } from "@langchain/langgraph";

const GraphState = Annotation.Root({
  document: Annotation<string>(),
  chunks: Annotation<string[]>(),
  currentIndex: Annotation<number>(),
  partialSummaries: Annotation<string[]>(),
  finalAnswer: Annotation<string>(),
});

type GraphStateType = typeof GraphState.State;
  1. Add a chunking node that splits the input document into manageable pieces. This example uses simple character-based splitting so you can run it without extra dependencies.
const chunkDocument = async (state: GraphStateType) => {
  const size = 4000;
  const chunks: string[] = [];

  for (let i = 0; i < state.document.length; i += size) {
    chunks.push(state.document.slice(i, i + size));
  }

  return {
    chunks,
    currentIndex: 0,
    partialSummaries: [],
  };
};
  1. Add a processing node that summarizes one chunk at a time using OpenAI. The model only sees the current chunk, which keeps token usage under control.
import { ChatOpenAI } from "@langchain/openai";

const llm = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

const summarizeChunk = async (state: GraphStateType) => {
  const chunk = state.chunks[state.currentIndex];

  const result = await llm.invoke([
    {
      role: "system",
      content: "Summarize this document chunk in 4 bullet points max.",
    },
    { role: "user", content: chunk },
  ]);

  return {
    partialSummaries: [...state.partialSummaries, result.content.toString()],
    currentIndex: state.currentIndex + 1,
  };
};
  1. Add routing logic so the graph keeps looping until every chunk is processed. Once the last chunk is done, move to the final merge step.
const routeNext = (state: GraphStateType) => {
  if (state.currentIndex >= state.chunks.length) {
    return "merge";
  }
  return "summarize";
};
  1. Merge all partial summaries into one final answer. In production systems this is where you would often extract risks, action items, or structured fields instead of plain prose.
const mergeSummaries = async (state: GraphStateType) => {
  const combined = state.partialSummaries.join("\n\n");

  const result = await llm.invoke([
    {
      role: "system",
      content:
        "Combine these partial summaries into one concise final summary with clear sections.",
    },
    { role: "user", content: combined },
  ]);

  return {
    finalAnswer: result.content.toString(),
  };
};
  1. Wire the graph together and run it against a long document string. This version is fully executable and shows the full flow end to end.
const graph = new StateGraph(GraphState)
  .addNode("chunk", chunkDocument)
  .addNode("summarize", summarizeChunk)
  .addNode("merge", mergeSummaries)
  .addEdge(START, "chunk")
  .addEdge("chunk", "summarize")
  .addConditionalEdges("summarize", routeNext, {
    summarize: "summarize",
    merge: "merge",
  })
  .addEdge("merge", END)
  .compile();

async function main() {
  const longDocument =
    "Section A... ".repeat(800) +
    "\n\nSection B... ".repeat(800) +
    "\n\nSection C... ".repeat(800);

  const result = await graph.invoke({
    document: longDocument,
    chunks: [],
    currentIndex: 0,
    partialSummaries: [],
    finalAnswer: "",
  });

  console.log(result.finalAnswer);
}

main();

Testing It

Run the script with npx tsx your-file.ts and confirm that it prints a merged summary instead of failing on context length. If you want to sanity-check the loop, temporarily log currentIndex inside summarizeChunk and verify it increments until it reaches chunks.length.

Test with a real long contract or policy excerpt next. You should see better stability than sending the whole document to one prompt because each call only handles one chunk.

If output quality drops, reduce chunk size or change the per-chunk prompt to extract structured fields like obligations, dates, exceptions, or risk flags.

Next Steps

  • Replace character-based splitting with token-aware splitting using LangChain text splitters.
  • Change the per-chunk step from summarization to extraction of JSON fields for downstream systems.
  • Add retries and persistence so interrupted runs can resume on large enterprise documents.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides