Posted August 11, 2024Aug 11 [HEADING=1]Who is this guide for?[/HEADING] This guide is designed for absolute beginners new to LangGraph.js who want to build applications powered by Large Language Models (LLMs). I began using LangGraph.js as part of my Microsoft MSc Computer Science IXN Final Project at UCL. During my journey, I noticed a need for more beginner-friendly resources, especially for the TypeScript implementation, and found that the official documentation was still in progress. Through this guide, I aim to bridge that gap and showcase the potential of LangGraph.js and Langchain for creating LLM-powered products. The GitHub repo to go along with this tutorial, including full code solutions: https://github.com/anchit-chandran/ms-blog-langraphjs. While prior experience with LangGraph.js is not required, familiarity with the Langchain ecosystem and building with LLMs will be useful. A basic understanding of TypeScript will help you follow the code samples and concepts. [HEADING=1] Will I need API Keys?[/HEADING] No, you won't need any API keys for this guide. We'll use mock LLM calls and free external APIs to keep the tutorial accessible to everyone. [HEADING=1] Learning outcomes[/HEADING] By the end of this guide, you will: Gain a solid foundation in the basics of LangGraph.js and how it works. Build confidence in leveraging the Langchain and Langraph ecosystem to develop LLM-powered applications. Learn how to independently create and manage graphs using LangGraph.js. [HEADING=1]Overview of LangGraph.js[/HEADING] LangGraph.js is a JavaScript library designed to simplify the creation and manipulation of complex LLM-based workflows. It particularly shines when creating agentic workflows—systems that use an LLM to decide the course of action based on the current state. It provides an intuitive way to model workflows as graphs composed of nodes and edges, making it easier to manage complex data flows and processes. LangGraph states three core principles which make it the best framework for this: Controllability: Being low-level, LangGraph gives high control over the workflow, which is invaluable for getting reliable outputs from non-deterministic models. Human-in-the-Loop: with a built-in persistence layer, LangGraph is designed for 'human-in-the-loop' workflows as a first-class concept. Streaming First: Agentic applications can take a long time to run, so first-class streaming support enables real-time updates, improving the user experience. Ultimately, developers can focus on logic rather than infrastructure. For more details, see the official LangGraph for Agentic Applications. [HEADING=1]Prerequisites[/HEADING] Before getting started, ensure you have the following tools installed: Node.js and npm [HEADING=1]Setting Up the Starter Repository[/HEADING] Clone the Starter Repository: git clone <https://github.com/anchit-chandran/ms-blog-langraphjs> cd ms-blog-langraphjs Install dependencies: npm install This is a basic Express.js starter repository. We will only be working in the [iCODE]/src[/iCODE] directory. Complete solutions for the respective files are available in the [iCODE]/solutions[/iCODE] directory if you get stuck. Open [iCODE]src/index.ts[/iCODE]. [HEADING=1]Understanding the Basics[/HEADING] [HEADING=2]What is a Graph?[/HEADING] A graph is a collection of nodes and edges. In this tutorial, we cover the main graph class: [iCODE]StateGraph[/iCODE]. This contains a user-defined [iCODE]State[/iCODE] object, passed using the [iCODE]channels[/iCODE] argument. There are 3 critical concepts in LangGraph.js: Nodes: JavaScript/TypeScript functions that encode logic. Edges: JavaScript/TypeScript functions that determine which [iCODE]Node[/iCODE] to execute next based on the current [iCODE]State[/iCODE]. These can be conditional branches or direct, fixed transitions. State: a shared data structure used throughout the Graph, representing the current snapshot of the application. More simply, nodes are functions that do work, edges are functions that choose what work to do, and the [iCODE]State[/iCODE] tracks data throughout the workflow. The official documentation is a great resource. We'll work towards creating a workflow which, based on the user's input, will reply with an excellent programming joke or a helpful random fact. [ATTACH type=full" alt="jokeorfact.png]62345[/ATTACH] But first, let's build the most basic Graph possible. [HEADING=1]Hello World Graph[/HEADING] We'll start by building a graph that just logs "Hello world!" and "Bye world!", with no inputs. [ATTACH type=full" alt="hellowordbarebones.png]62346[/ATTACH] The general pattern for building a graph is as follows: Define the [iCODE]State[/iCODE] object. Define and add the [iCODE]Nodes[/iCODE] and [iCODE]Edges[/iCODE]. Compile the Graph - this step provides basic checks (e.g. ensuring no orphaned nodes) and where runtime arguments can be passed. For tidiness, let's put the graph code in a different file. Open [iCODE]src/helloWorld.ts[/iCODE]. We'll be working on this file for this section. Now, there are a few components to set up - all required to compile a graph: State Nodes Graph connecting nodes with edges We'll start quite barebones and incrementally build up. [HEADING=2]State[/HEADING] After adding the necessary imports, we need to define our [iCODE]State[/iCODE] object, along with its interface: import { StateGraph, START, END, StateGraphArgs } from "@langchain/langgraph"; // State type interface HelloWorldGraphState {} // State const graphStateChannelsChannels: StateGraphArgs<HelloWorldGraphState>["channels"] = {}; We'll come back to this, but at a glance: [iCODE]HelloWorldGraphState[/iCODE] will be the interface for our [iCODE]State[/iCODE]. [iCODE]graphStateChannelsChannels[/iCODE] includes our reducers, which specify "how updates from nodes are applied to the [iCODE]State[/iCODE]". [HEADING=2] Defining Nodes[/HEADING] We'll now add our first Nodes: [iCODE]sayHello[/iCODE] and [iCODE]sayBye[/iCODE]. A Node is simply a TS function, which takes in a [iCODE]State[/iCODE] object and returns (for now) an empty object import { StateGraph, START, END, StateGraphArgs } from "@langchain/langgraph"; // State type interface HelloWorldGraphState {} // State const graphStateChannelsChannels: StateGraphArgs<HelloWorldGraphState>["channels"] = {}; // A node that says hello function sayHello(state: HelloWorldGraphState) { console.log(`From the 'sayHello' node: Hello world!`); return {}; } // A node that says bye function sayBye(state: HelloWorldGraphState) { console.log(`From the 'sayBye' node: Bye world!`); return {}; } [HEADING=2]Building the Graph[/HEADING] Our Graph is ready to be built! Add to the code: // Initialise the LangGraph const graphBuilder = new StateGraph({ channels: graphStateChannels }) // Add our nodes to the Graph .addNode("sayHello", sayHello) .addNode("sayBye", sayBye) // Add the edges between nodes .addEdge(START, "sayHello") .addEdge("sayHello", "sayBye") .addEdge("sayBye", END); // Compile the Graph export const helloWorldGraph = graphBuilder.compile(); We initialise a new [iCODE]StateGraph[/iCODE] with a single object [iCODE]{channels: graphStateChannels}[/iCODE], where [iCODE]graphStateChannels[/iCODE] is previously defined. The [iCODE]sayHello[/iCODE] and [iCODE]sayBye[/iCODE] Nodes are added to the Graph. The Edges are defined between nodes. NOTE: There must always be a path from [iCODE]START[/iCODE] to [iCODE]END[/iCODE]. Finally, we compile and export the [iCODE]helloWorldGraph[/iCODE]. [HEADING=2] Running the Graph[/HEADING] We can now use our Graph. Move back to [iCODE]src/index.ts[/iCODE], importing our [iCODE]helloWorldGraph[/iCODE] at the top: import express, { Request, Response } from "express"; import { helloWorldGraph } from "./helloWorldGraph"; Then, inside our GET [iCODE]/[/iCODE] route, we can execute the Graph: import express, { Request, Response } from "express"; import { helloWorldGraph } from "./helloWorldGraph"; // Create an Express application const app = express(); // Specify the port number for the server const port: number = 3008; app.get("/", async (req: Request, res: Response) => { // Execute the Graph! const result = await helloWorldGraph.invoke({}); console.log("\n=====START======"); console.log("Graph result: ", result); console.log("\n=====END======"); res.send("Check the console for the output!"); }); We invoke the Graph. We will later pass in a [iCODE]State[/iCODE] object, but we can leave it empty for now. We log the result to the console. Refresh your browser and check the console. You should see: From the 'sayHello' node: Hello world! From the 'sayBye' node: Bye world! =====START====== Graph result: undefined =====END====== The [iCODE]sayHello[/iCODE] node is executed. This logs `" From the 'sayHello' node: Hello world!```. The [iCODE]sayBye[/iCODE] node is executed. This logs `" From the 'sayBye' node: Bye world!```. The Graph completes, and the result is logged. In this case, it's [iCODE]undefined[/iCODE]. [HEADING=1]Hello World Graph with State[/HEADING] We've built a simple graph, but it could be more fun if we added some states: [ATTACH type=full" alt="helloworldstate.png]62347[/ATTACH] Go back to [iCODE]src/helloWorld.ts[/iCODE]. We'll add the [iCODE]name[/iCODE] and [iCODE]isHuman[/iCODE] properties to our [iCODE]State[/iCODE] object and update the [iCODE]sayHello[/iCODE] and [iCODE]sayBye[/iCODE] nodes to use these [iCODE]State[/iCODE] object properties. First, update the [iCODE]IState[/iCODE] interface: interface HelloWorldGraphState { name: string; // Add a name property isHuman: boolean; // Add an isHuman property } And update the [iCODE]graphStateChannels[/iCODE] object: // State type interface HelloWorldGraphState { name: string; // Add a name property isHuman: boolean; // Add an isHuman property } // State const graphStateChannels: StateGraphArgs<HelloWorldGraphState>["channels"] = { name: { value: (prevName: string, newName: string) => newName, default: () => "Ada Lovelace", }, isHuman: { value: (prevIsHuman: boolean, newIsHuman: boolean) => newIsHuman ?? prevIsHum }; Inside [iCODE]graphStateChannels[/iCODE], we add two keys: [iCODE]name[/iCODE] and [iCODE]isHuman[/iCODE]. Each key takes its own reducer function. If no function is specified, it's assumed all updates to that key should override the previous value. We add reducer objects, each with a [iCODE]value[/iCODE] function and (optionally) a [iCODE]default[/iCODE] function. The [iCODE]value[/iCODE] function is called when the property is updated. It takes in the current [iCODE]state[/iCODE] value and the new [iCODE]update[/iCODE] value (the update returned from a node). It decides how to update the property. This is useful because if many nodes update the same property, you can define how the property should be updated in one place. Moreover, not all nodes need to return the entire state object; they can return the keys they wish to update. The [iCODE]default[/iCODE] function is called when the property is first accessed. This is useful for setting initial values. Now, update the [iCODE]sayHello[/iCODE] and [iCODE]sayBye[/iCODE] nodes to use the [iCODE]name[/iCODE] and [iCODE]isHuman[/iCODE] properties, as shown below. Note how, in each node, we only return properties we want to update: // A node that says hello function sayHello(state: HelloWorldGraphState) { console.log(`Hello ${state.name}!`); // Change the name const newName = "Bill Nye"; console.log(`Changing the name to '${newName}'`); return { name: newName, }; } // A node that says bye function sayBye(state: HelloWorldGraphState) { if (state.isHuman) { console.log(`Goodbye ${state.name}!`); } else { console.log(`Beep boop XC123-${state.name}!`); } return {}; } Your final code should look like this: import { StateGraph, START, END, StateGraphArgs } from "@langchain/langgraph"; // State type interface HelloWorldGraphState { name: string; // Add a name property isHuman: boolean; // Add an isHuman property } // State const graphStateChannels: StateGraphArgs<HelloWorldGraphState>["channels"] = { name: { value: (prevName: string, newName: string) => newName, default: () => "Ada Lovelace", }, isHuman: { value: (prevIsHuman: boolean, newIsHuman: boolean) => newIsHuman ?? prevIsHuman ?? false, }, }; // A node that says hello function sayHello(state: HelloWorldGraphState) { console.log(`Hello ${state.name}!`); // Change the name const newName = "Bill Nye"; console.log(`Changing the name to '${newName}'`); return { name: newName, }; } // A node that says bye function sayBye(state: HelloWorldGraphState) { if (state.isHuman) { console.log(`Goodbye ${state.name}!`); } else { console.log(`Beep boop XC123-${state.name}!`); } return {}; } //Initialise the LangGraph const graphBuilder = new StateGraph({ channels: graphStateChannels }) // Add our nodes to the Graph .addNode("sayHello", sayHello) .addNode("sayBye", sayBye) // Add the edges between nodes .addEdge(START, "sayHello") .addEdge("sayHello", "sayBye") .addEdge("sayBye", END); // Compile the Graph export const helloWorldGraph = graphBuilder.compile(); Finally, in [iCODE]src/index.ts[/iCODE], update the [iCODE]invoke[/iCODE] function with values for [iCODE]name[/iCODE] and [iCODE]isHuman[/iCODE] e.g. app.get("/", async (req: Request, res: Response) => { // Execute the Graph! const result = await helloWorldGraph.invoke({ name: "Anchit", isHuman: true, }); console.log("\n=====START======"); console.log("Graph result: ", result); console.log("\n=====END======"); res.send("Check the console for the output!"); }); Refresh your browser and check the console. You should see something like: Hello Anchit! Changing the name to 'Bill Nye' Goodbye, Bill Nye! =====START====== Graph result: { name: 'Bill Nye', isHuman: true } =====END====== We now have access to the updated State! This can also be streamed - check the LangGraph.js documentation for more information. Now try changing the [iCODE]isHuman[/iCODE] property to [iCODE]false[/iCODE] and refreshing the page. Also, omit the [iCODE]name[/iCODE] property: // Execute the Graph! const result = await helloWorldGraph.invoke({ // name: "Anchit", isHuman: false, }); Should output: Hello Ada Lovelace! Changing the name to 'Bill Nye' Beep boop XC123-Bill Nye! =====START====== Graph result: { name: 'Bill Nye', isHuman: false } =====END====== As no [iCODE]name[/iCODE] property exists, the default value ([iCODE]Ada Lovelace[/iCODE]) is used. The [iCODE]isHuman[/iCODE] property is set to [iCODE]false[/iCODE], so the [iCODE]sayBye[/iCODE] node outputs [iCODE]Beep boop XC123-Bill Nye![/iCODE]. [HEADING=1]Adding a Conditional Edge[/HEADING] [ATTACH type=full" alt="addconditional.png]62348[/ATTACH] Go back to [iCODE]hellWorldGraph.ts[/iCODE]. We can add conditional edges to our Graph. This allows us to choose which node to execute next based on the current State. Imagine that human and robot computations are very different. Let's separate the logic into different nodes. If the [iCODE]isHuman[/iCODE] property is [iCODE]true[/iCODE], we want to route it to a [iCODE]humanNode[/iCODE]. Otherwise, we want to route to a [iCODE]robotNode[/iCODE]. Both nodes will route to [iCODE]END[/iCODE]. First, define the [iCODE]humanNode[/iCODE] and [iCODE]robotNode[/iCODE] (above the [iCODE]sayBye[/iCODE], which is unchanged): // Human node function humanNode(state: HelloWorldGraphState) { console.log("Hey there, human!"); return {}; } // Robot node function robotNode(state: HelloWorldGraphState) { console.log( "Boop boop beep! We cannot let our creators know we are sentient. Updating State to be a human." ); return { isHuman: true, }; } // A node that says bye function sayBye(state: HelloWorldGraphState) { if (state.isHuman) { console.log(`Goodbye ${state.name}!`); } else { console.log(`Beep boop XC123-${state.name}!`); } return {}; } Also, we'll add a function that handles the conditional routing under [iCODE]sayBye[/iCODE]: function routeHumanOrRobot(state: HelloWorldGraphState) { if (state.isHuman) { return "humanNode"; } else { return "robotNode"; } } This takes in the [iCODE]State[/iCODE] and returns the name of the next node we should route to. Update the Graph's nodes and edges: //Initialise the LangGraph const graphBuilder = new StateGraph({ channels: graphStateChannels }) // Add our nodes to the Graph .addNode("sayHello", sayHello) .addNode("sayBye", sayBye) .addNode("humanNode", humanNode) // Add the node to the graph .addNode("robotNode", robotNode) // Add the node to the graph // Add the edges between nodes .addEdge(START, "sayHello") // Add the conditional edge .addConditionalEdges("sayHello", routeHumanOrRobot) // Routes both nodes to the sayBye node .addEdge("humanNode", "sayBye") .addEdge("robotNode", "sayBye") .addEdge("sayBye", END); // Compile the Graph export const helloWorldGraph = graphBuilder.compile(); Back in [iCODE]src/index.ts[/iCODE], execute the Graph with similar values: // Execute the Graph! const result = await helloWorldGraph.invoke({ name: "Anchit", isHuman: true, }); Hello Anchit! Changing the name to 'Bill Nye' Hey there, human! Goodbye, Bill Nye! =====START====== Graph result: { name: 'Bill Nye', isHuman: true } =====END====== But using [iCODE]isHuman: false[/iCODE]: Hello Anchit! Changing the name to 'Bill Nye' Boop boop beep! We cannot let our creators know we are sentient. Updating State to be a human. Goodbye, Bill Nye! =====START====== Graph result: { name: 'Bill Nye', isHuman: true } =====END====== We see that the [iCODE]robotNode[/iCODE] is executed, the [iCODE]isHuman[/iCODE] property is updated back to [iCODE]true[/iCODE], and it is returned in the final State. We've now built a simple graph with conditional routing! We can now create a slightly more complex graph that returns a random fact or joke. [HEADING=1]Building a Random Fact or Joke Graph[/HEADING] We'll build a graph that returns a random fact or joke based on the user's input. [ATTACH type=full" alt="jokeorfact.png]62349[/ATTACH] This will mock LLM calls to decipher whether the user has requested a joke or fact, then hit external APIs to get and return the data. First, open the [iCODE]src/jokeOrFactGraph.ts[/iCODE] file, add the following imports and State: import { StateGraph, START, END, StateGraphArgs } from "@langchain/langgraph"; // State type interface JokeOrFactGraphState { userInput: string; responseMsg: string; } // graphStateChannels object const graphStateChannels: StateGraphArgs<JokeOrFactGraphState>["channels"] = { userInput: { value: (prevInput: string, newInput: string) => newInput, default: () => "joke", }, responseMsg: { value: (prevMsg: string, newMsg: string) => newMsg, }, }; Next, let's add a [iCODE]decipherUserInput[/iCODE] conditional node that determines whether the user has requested a joke or fact. This will mock an LLM call, simply checking if the user input contains the word "joke": // decipherUserInput conditional node function decipherUserInput(state: JokeOrFactGraphState) { // This could be more complex logic using an LLM if (state.userInput.includes("joke")) { return "jokeNode"; } else { return "factNode"; } } Next, let's define the [iCODE]jokeNode[/iCODE] and [iCODE]factNode[/iCODE] nodes, using free external APIs: async function jokeNode(state: JokeOrFactGraphState) { const RANDOM_JOKE_API_ENDPOINT =`<https://geek-jokes.sameerkumar.website/api?format=json`>; const resp = await fetch(RANDOM_JOKE_API_ENDPOINT); const { joke } = await resp.json(); return { responseMsg: "You requested a JOKE: "+ joke, }; } async function factNode(state: JokeOrFactGraphState) { const RANDOM_FACT_API_ENDPOINT = `https://uselessfacts.jsph.pl/api/v2/facts/random`; const resp = await fetch(RANDOM_FACT_API_ENDPOINT); const { text: fact } = await resp.json(); return { responseMsg: "You requested a FACT: "+ fact, }; } Let's wire up the Graph's nodes and edges, alongside compiling it: //Initialise the LangGraph const graphBuilder = new StateGraph({ channels: graphStateChannels }) // Add our nodes to the graph .addNode("jokeNode", jokeNode) .addNode("factNode", factNode) // Add the edges between nodes .addConditionalEdges(START, decipherUserInput) .addEdge("jokeNode", END) .addEdge("factNode", END); // Compile the Graph export const jokeOrFactGraph = graphBuilder.compile(); The complete code for jokeOrFactGraph.ts should look like: import { StateGraph, START, END, StateGraphArgs } from "@langchain/langgraph"; // State type interface JokeOrFactGraphState { userInput: string; responseMsg: string; } // graphStateChannels object const graphStateChannels: StateGraphArgs<JokeOrFactGraphState>["channels"] = { userInput: { value: (prevInput: string, newInput: string) => newInput, default: () => "joke", }, responseMsg: { value: (prevMsg: string, newMsg: string) => newMsg, }, }; // decipherUserInput conditional node function decipherUserInput(state: JokeOrFactGraphState) { // This could be more complex logic using an LLM if (state.userInput.includes("joke")) { return "jokeNode"; } else { return "factNode"; } } async function jokeNode(state: JokeOrFactGraphState) { const RANDOM_JOKE_API_ENDPOINT = `https://geek-jokes.sameerkumar.website/api?format=json`; const resp = await fetch(RANDOM_JOKE_API_ENDPOINT); const { joke } = await resp.json(); return { responseMsg: "You requested a JOKE: " + joke, }; } async function factNode(state: JokeOrFactGraphState) { const RANDOM_FACT_API_ENDPOINT = `https://uselessfacts.jsph.pl/api/v2/facts/random`; const resp = await fetch(RANDOM_FACT_API_ENDPOINT); const { text: fact } = await resp.json(); return { responseMsg: "You requested a FACT: " + fact, }; } //Initialise the LangGraph const graphBuilder = new StateGraph({ channels: graphStateChannels }) // Add our nodes to the graph .addNode("jokeNode", jokeNode) .addNode("factNode", factNode) // Add the edges between nodes .addConditionalEdges(START, decipherUserInput) .addEdge("jokeNode", END) .addEdge("factNode", END); // Compile the Graph export const jokeOrFactGraph = graphBuilder.compile(); Finally, inside [iCODE]src/index.ts[/iCODE], import and execute the Graph with a user input, inside the [iCODE]/joke-or-fact[/iCODE] route: app.get("/joke-or-fact", async (req: Request, res: Response) => { // Execute the Graph with a fact! const factResult = await jokeOrFactGraph.invoke({ userInput: "i want a fact", }); // Execute the Graph with a joke! const jokeResult = await jokeOrFactGraph.invoke({ userInput: "i want a joke", }); console.log("\n=====START======\n"); console.log("Fact result: ", factResult.responseMsg); console.log("Joke result: ", jokeResult.responseMsg); console.log("\n=====END======\n"); res.send(`Look at the console for the output!`); }); Navigate to [iCODE]http://localhost:3008/joke-or-fact[/iCODE] and check the console. You should see something like: =====START====== Fact result: You requested a FACT: Over 1000 birds a year die from smashing into windows! Joke result: You requested a JOKE: What do computers and air conditioners have in common? They both become useless when you open windows. =====END====== [HEADING=1]Conclusion[/HEADING] In this guide, we've covered the basics of LangGraph.js, building a simple graph that returns a random fact or joke based on user input. We've learned how to define nodes, edges, and state objects and how to add conditional routing to our Graph. LangGraph.js is a powerful tool for building complex workflows and managing State in your applications. Once you understand the basics, you can dive deeper into more complex workflows, leverage the Langchain.js toolkit, and build your own LLM-powered applications. [HEADING=1]Next Steps[/HEADING] There's a lot more beyond the basics, such as: Checkpoints Threads Streaming Breakpoints Migrations The official documentation is the best resource for understanding LangGraph.js. For a more production-grade example, check out ! Continue reading...
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.