Skip to content

Core Library: Conceptual Guide

Graphs

At its core, LangGraph4j models agent workflows as graphs. You define the behavior of your agents using three key components:

  1. State: A shared data structure that represents the current snapshot of your application. It is represented by an AgentState object.

  2. Nodes: A Functional Interface (AsyncNodeAction) that encode the logic of your agents. They receive the current State as input, perform some computation or side-effect, and return a request for updated State.

  3. Edges: A Functional Interface (AsyncCommandAction) that determine which Node to execute next based on the current State. They can be conditional branches or fixed transitions.

    It also can equest for updated State 👀.

By composing Nodes and Edges, you can create complex, looping workflows that evolve the State over time. The real power, though, comes from how LangGraph4j manages that State. To emphasize: Nodes and Edges are like functions - they can contain an LLM or just Java code.

In short: nodes do the work. edges tell what to do next.

StateGraph

The StateGraph class is the main graph class to uses. This is parameterized by a user defined State object.

Compile your graph

To build your graph, you first define the state, you then add nodes and edges, and then you compile it. What exactly is compiling your graph and why is it needed?

Compiling is a pretty simple step. It provides a few basic checks on the structure of your graph (no orphaned nodes, etc). It is also where you can specify runtime args like checkpointers and Interruptions. You compile your graph by just calling the .compile method:

CompileConfig compileConfig = ....
// compile your graph
var graph = graphBuilder.compile( CompileConfig );

‼️ You MUST compile your graph before you can use it.

CompileConfig

The CompileConfig class allows you to pass configuration parameters that control runtime behaviors of your graph. You build a configuration using the builder pattern and pass it to the .compile() method.

var compileConfig = CompileConfig.builder()
                    .checkpointSaver(mySaver)
                    .recursionLimit(50)
                    .interruptBefore("nodeA", "nodeB")
                    .build();

var graph = graphBuilder.compile(compileConfig);
Configuration Attributes
Attribute Type Default Description
checkpointSaver BaseCheckpointSaver null The checkpoint saver implementation for persisting graph state across executions. Required for stateful features like human-in-the-loop workflows and resuming interrupted graphs. See Checkpointer section for more details.
interruptBefore Set<String> empty Node names where the graph should pause before executing the node. Useful for inspecting state before a node runs or for getting human approval before proceeding.
interruptsAfter Set<String> empty Node names where the graph should pause after executing the node. Useful for inspecting results after a node completes or for user feedback.
interruptBeforeEdge boolean false If true, interruptions at a node occur after it executes but before any conditional edges are evaluated. This allows inspecting the state before the graph branches to the next node.
recursionLimit int 25 Default maximum number of execution steps allowed for each graph invocation. Prevents infinite loops by raising an error if the limit is exceeded. A RunnableConfig recursion limit overrides this default for one invocation.
releaseThread boolean false If true, the checkpointer will release all data associated to the current thread acquired during graph execution.
graphId String null Optional identifier for the graph. Useful for logging, monitoring, or distinguishing between multiple graph instances. It will available through RunnableConfig.graphId()

Execute your graph

Once you have compiled your graph, you can execute it in two different modes: synchronous and asynchronous.

Synchronous Execution

For synchronous execution, use the execute method to run the graph and get the final result directly:

var config = RunnableConfig.builder()
                          .threadId("thread-1")
                          .build();

Optional<State> state = graph.invoke( GraphInput.noArgs(), config);
System.out.println( state.orElse(null) );

Optional<NodeOutput<State>> output = graph.invokeFinal(GraphInput.noArgs(), config);
System.out.println( output.orElse(null) );

The invoke() method blocks until the graph completes and returns the final state of the graph. The invokeFinal() method blocks until the graph completes and returns the final node output of the graph.

Asynchronous Execution

For more detailed visibility into the execution flow, use the stream method which returns an [AsyncGenerator] that allows you to iterate over each step executed in the workflow:

var config = RunnableConfig.builder()
                          .threadId("thread-1")
                          .build();

compiledGraph.stream(inputs, config) // start streaming outputs from the graph execution
        .forEachAsync( output -> // process each output as it is produced
            System.out.println("Step executed: " + output))
        .thenApply( GraphResult::from ) // holds final result in GraphResult
        .thenAccept( result -> { // process the final result
            if (result.isStateDataOrCheckpointSaverTag()) {
                System.out.println("Final state: " + result.asStateDataOrLastCheckpointStateData());
            } else if (result.isInterruptionMetadata()) {
                System.out.println("Graph interrupted: " + result.asInterruptionMetadata());
            }
        });

The stream() method returns an AsyncGenerator that yields the state updates after each node execution. This is particularly useful for:

  • Streaming updates: Monitor intermediate states as the workflow progresses
  • Real-time feedback: Display each step to users as it executes
  • Debugging: Inspect how the state evolves throughout execution
  • Interruption handling: Access interruption metadata when interruption are triggered

You can also retrieve the final result value from the generator, for details take a look to GraphResult section:

RunnableConfig

The RunnableConfig class carries configuration parameters through graph execution, making runtime information available to both nodes (AsyncNodeActionWithConfig) and conditional edges (AsyncCommandAction). This allows you to pass context-specific data and control execution behavior at runtime without modifying the graph structure.

You provide a RunnableConfig when invoking the graph:

var config = RunnableConfig.builder()
                          .threadId("user-123")
                          .streamMode(CompiledGraph.StreamMode.UPDATES)
                          .recursionLimit(50)
                          .putMetadata("userId", "user-123")
                          .putMetadata("model", "gpt-4")
                          .build();

graph.stream(inputs, config);
Configuration Builder
Method Description
threadId( String ) A unique identifier for the execution thread/session. Essential for checkpoint-based persistence, as it groups related executions together. Allows resuming interrupted graphs or maintaining conversation history.
checkPointId( String ) Specific checkpoint identifier within a thread. Useful for resuming execution from a specific point rather than from the beginning. It is managed by checkpoint saver
nextNode( String ) Specifies which node should execute next. Primarily used internally by the graph engine when resuming interrupted executions.
streamMode( CompiledGraph.StreamMode ) Controls how results are streamed during execution. The default is VALUES; SNAPSHOTS is also available when checkpoint snapshots are required.
recursionLimit( Integer ) Optional maximum number of execution steps for this invocation. When absent, the graph uses its configured default. The builder rejects values less than or equal to zero.
addParallelNodeExecutor( String nodeId, Executor) stores an internal executor under a generated key in the form __PARALLEL__(nodeId). Use that helper rather than writing the key directly.
disableCloneState() Disables state cloning during graph running
putMetadata( String key, Object value ) Add or replace a custom metadata entry to the configuration.
addMetadata( String key, Object value ) Adds a custom metadata entry to the configuration. Raise an error if key already exists.
Reserved attributes' metadata

RunnableConfig provides metadata entries that are reserved by the runtime and should not be overwritten:

Metadata key Value type Accessor Purpose
LG4j_STUDIO_MDK Boolean config.isRunningInStudio() Internal flag indicating that the graph is running in Studio.
LG4j_NODE_ID String config.nodeId() Current executing node ID, injected by the runtime.
LG4j_GRAPH_PATH GraphPath config.graphPath() (deprecated) Legacy graph/subgraph path for nested executions. Use nodePath() for the current node path.
LG4j_GRAPH_NODE_PATH GraphPath config.nodePath() Current node path within the graph hierarchy.
LG4j_GRAPH_ID String config.graphId() Effective graph ID propagated at runtime for tracing and logging.
LG4j_SUBGRAPH_UPDATE_DATA Map<String, Object> Internal Subgraph resume/update data. Do not use in application metadata.
LG4J_CUSTOM_DISPATCHER GraphDefinition.Dispatcher config.customDispatcher() Internal dispatcher used to dispatch custom node output by node action.
LG4j_DISABLE_CLONE_STATE Boolean config.isCloneStateDisabled() Disables state cloning when set to true; exposed state may then reflect subsequent mutations.
Accessing RunnableConfig in Nodes and Edges

Since nodes and edges are functional interfaces, you can access the configuration through:

In AsyncNodeActionWithConfig:

AsyncNodeActionWithConfig<MyState> node = (state, config) -> {
    // Access thread ID
    var threadId = config.threadId().orElse("default");

    // Access metadata
    String userId = (String) config.metadata("userId").orElse("anonymous");
    String model = (String) config.metadata("model").orElse("gpt-3.5");

    // Access optional graph ID
    var graphId = config.graphId();

    // Use configuration in your logic
    System.out.printf("Executing for user: %s with model: %s%n", userId, model);

    return Map.of("result", "processed");
};

In AsyncCommandAction (conditional edges):

AsyncCommandAction<MyState> router = (state, config) -> {
    String userId = (String) config.metadata("userId").orElse("anonymous");

    // Route based on runtime configuration
    if ("premium-user".equals(userId)) {
        return "premium-path";
    } else {
        return "standard-path";
    }
};
Common Use Cases
  • User Context: Pass user ID, tenant ID, or organizational context through execution
  • Feature Flags: Enable/disable features at runtime via metadata
  • Model Selection: Choose different LLM models based on metadata
  • Logging & Monitoring: Use graphId() and threadId() for tracing and debugging
  • Session Resumption: Use threadId() to fetch and resume previous execution state
  • Concurrent Control: Configure custom executors for parallel nodes via metadata
Building RunnableConfig
// Minimal configuration
var config = RunnableConfig.builder()
                          .threadId("thread-1")
                          .build();

// Rich configuration with metadata
var config = RunnableConfig.builder()
                          .threadId("conversation-user-123")
                          .streamMode(CompiledGraph.StreamMode.UPDATES)
                          .putMetadata("userId", "user-123")
                          .putMetadata("tenantId", "org-456")
                          .putMetadata("llmModel", "gpt-4-turbo")
                          .putMetadata("isVip", true)
                          .build();

GraphResult

The final result returned by AsyncGenerator from the stream method is a generic Object that can contain different types of values depending on what was yielded during the graph execution. Rather than constantly checking the type with instanceof, LangGraph4j provides the GraphResult utility class to safely identify and retrieve the correct result type.

Result Types

The GraphResult class can wrap the following result types:

Type Description
STATE_DATA A Map<String, Object> representing the state snapshot after a node execution. This is the most common result during graph streaming.
NODE_OUTPUT A NodeOutput object containing detailed information about a node's execution, including the node ID, the resulting state, and execution metadata.
INTERRUPTION_METADATA An InterruptionMetadata object indicating that the graph execution was interrupted. This contains information about why the graph was interrupted and the state at the interruption point.
CHECKPOINT_SAVER_TAG A BaseCheckpointSaver.Tag object representing a checkpoint identifier and metadata. Useful for tracking persisted states.
EMPTY Indicates no result was produced (typically when the stream yields null).
Using GraphResult

Instead of manually checking types, use GraphResult to safely extract the result:

// Get the final result direct from the generator
GraphResult finalResult = GraphResult.from(generator);

if ( finalResult.isEmpty() ) {
    System.out.println("result is empty");
}
else if (finalResult.isStateDataOrCheckpointSaverTag()) {
    Map<String, Object> state = finalResult.asStateDataOrLastCheckpointStateData();
    System.out.printf("Graph completed with state: %s%n", state);
} else if (finalResult.isNodeOutput()) {
    NodeOutput<?> output = finalResult.asNodeOutput();
    System.out.printf("Graph completed with node: %s%n", output.nodeId());
} else if (finalResult.isInterruptionMetadata()) {
    InterruptionMetadata<?> metadata = finalResult.asInterruptionMetadata();
    System.out.printf("Graph completed with interruption: %s%n", metadata);
}

The GraphResult class provides type-safe methods to check (isStateDataOrCheckpointSaverTag(), isNodeOutput(), etc.) and retrieve (asStateDataOrLastCheckpointStateData(), asNodeOutput(), etc.) each result type, preventing casting errors and making your code more maintainable.

State

The first thing you do when you define a graph is define the State of the graph. The State consists of the schema of the graph as well as reducer functions which specify how to apply updates to the state. The schema of the State will be the input schema to all Nodes and Edges in the graph, and should be defined using a map of [Channel] object. All Nodes will emit updates to the State which are then applied using the specified reducer function.

Schema

The way to specify the schema of a graph is by defining map of Channel objects where each key is an item in the state. If no Channel is specified for an item then it is assumed that all updates to that item should override it.

Reducers

Reducers are key to understanding how updates from nodes are applied to the State. Each key in the State has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. Let's take a look at a few examples to understand them better.

Example A:

static class MessagesState extends AgentState {

    static Map<String, Channel<?>> SCHEMA = Map.of(
            "messages", Channels.appender(ArrayList::new)
    );
}

var graphBuilder = new StateGraph<>( MessagesState.SCHEMA, MessagesState::new)

AppenderChannel

In the example above we specify for messages property a built-in channel AppenderChannel which use a Reducer implementation to accumulate values.

Remove Messages

AppenderChannel supports the message deletion throught its nested functional interface RemoveIdentifier. Inheriting such interface you can create a particular value that when will be put inside a State's property, with AppenderChannel schema, instrucs the Reducer to remove the element that match the specified conditions in RemoveIdentifier throught compareTo( element, index ) method.

RemoveByHash

Langgraph4j provides a Built in [AppederChannel.RemoveIdentifier] named RemoveByHash that allow to remove messages comparing their hashCode, below an example of its usage:

var workflow = new StateGraph<>(MessagesState.SCHEMA, MessagesState::new)
        .addNode("agent_1", node_async(state -> Map.of("messages", "message1")))
        .addNode("agent_2", node_async(state -> Map.of("messages", List.of("message2", "message2.1"))))
        .addNode("agent_3", node_async(state -> 
            Map.of("messages", RemoveByHash.of("message2.1")) // this remove "message2.1" from messages values
        ))
        .addEdge("agent_1", "agent_2")
        .addEdge("agent_2", "agent_3")
        .addEdge(START, "agent_1")
        .addEdge("agent_3", END);
ReplaceAllWith

Langgraph4j provides a built in [AppederChannel.ReplaceAllWith] that allow to replace all elements with new ones, below an example of its usage:

var workflow = new StateGraph<>(MessagesState.SCHEMA, MessagesState::new)
        .addNode("agent_1", node_async(state -> Map.of("messages", "message1")))
        .addNode("agent_2", node_async(state -> Map.of("messages", List.of("message2", "message2.1"))))
        .addNode("agent_3", node_async(state -> 
            Map.of("messages", ReplaceAllWith.of( List.of("a1", "a2"))) // this replace current messages values with ["a1", "a2"]
        ))
        .addEdge("agent_1", "agent_2")
        .addEdge("agent_2", "agent_3")
        .addEdge(START, "agent_1")
        .addEdge("agent_3", END);

Custom Reducer

You can also specify a custom reducer for a particular state property

Example B:

static class MyState extends AgentState {

    static Map<String, Channel<?>> SCHEMA = Map.of(
            "property", Channel.<String>of( ( oldValue, newValue ) -> newValue.toUpperCase() )
    );
}

var graphBuilder = new StateGraph<>( MessagesState.SCHEMA, MyState::new)

Serializer

During graph execution the state needs to be serialized (mostly for cloning purpose) also for providing ability to persist the state across different executions. To do this we have provided a new streighforward implementation based on Serializer interface.

Why create a new Serialization framework ?

  1. Doesn't rely on unsafe standard serialization framework.
  2. Allow to implement serialization also to third-party (non serializable) classes
  3. Avoid as much as possible class loading problem
  4. Manage nullable value in serialization process

Features

  • [x] Allow to serialize using the java built-in standard binary serialization technique
  • [x] Allow to plug also different serialization techniques

Currently the main class for state's serialization using built-in java stream is ObjectStreamStateSerializer. It is also available an abstraction allowing to plug serialization techniques text based like JSON and/or YAML that is PlainTextStateSerializer.

Out of the Box

There are several provided Serializers out-of-the-box:

class description
ListSerializer built-in List<Object> serializer
MapSerializer built-in Map<String,Object> serializer
   
AiMessageSerializer langchain4j AiMessage Serializer
ChatMesssageSerializer langchain4j ChatMesssage Serializer
SystemMessageSerializer langchain4j SystemMessage Serializer
UserMessageSerializer langchain4j UserMessage Serializer
ToolExecutionRequestSerializer langchain4j ToolExecutionRequest Serializer
ToolExecutionResultMessageSerializer langchain4j ToolExecutionResultMessage Serializer

Nodes

In LangGraph4j, nodes are typically a Functional Interface (AsyncNodeAction) where the argument is the state, you add these nodes to a graph using the addNode method:

public class State extends AgentState {

  public State(Map<String, Object> initData) {
    super( initData  );
  }

  Optional<String> input() { return value("input"); } 
  Optional<String> results() { return value("results"); } 

}

AsyncNodeAction<State> myNode = node_async(state -> {
    System.out.println( "In myNode: " );
    return Map.of( results: "Hello " + state.input().orElse( "" ) );  
});

AsyncNodeAction<State> myOtherNode = node_async(state -> state);

var builder = new StateGraph( State::new )
  .addNode("myNode", myNode)
  .addNode("myOtherNode", myOtherNode)

Since AsyncNodeAction is designed to work with CompletableFuture, you can use node_async static method that adapt it to a simpler syncronous scenario.

START Node

The START Node is a special node that represents the node sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.

import static org.bsc.langgraph4j.StateGraph.START;

graph.addEdge(START, "nodeA");

END Node

The END Node is a special node that represents a terminal node. This node is referenced when you want to denote which edges have no actions after they are done.

import static org.bsc.langgraph4j.StateGraph.END;

graph.addEdge("nodeA", END);

Edges

Edges define how the logic is routed and how the graph decides to stop. This is a big part of how your agents work and how different nodes communicate with each other. There are a few key types of edges:

  • Normal Edges:

    Go directly from one node to the next.

  • Conditional Edges:

    Call a function to determine which node(s) to go to next.

  • Entry Point:

    Which node to call first when user input arrives.

  • Conditional Entry Point:

    Call a function to determine which node(s) to call first when user input arrives.

Normal Edges

If you always want to go from node A to node B, you can use the addEdge method directly.

// add a normal edge
graph.addEdge("nodeA", "nodeB");

Conditional Edges

If you want to optionally route to 1 or more edges (or optionally terminate), you can use the addConditionalEdges method. This method accepts the name of a node and a Functional Interface (AsyncCommandAction) that will be used as " routing function" to call after that node is executed:

graph.addConditionalEdges("nodeA", routingFunction, 
        EdgeMapping.builder()
          .to( "nodeB", "first")
          .to( "nodeC", "second" )
          .build());

Similar to nodes, the routingFunction accept the current state of the graph and return a string value.

You must provide an object that maps the routingFunction's output to the name of the next node.

Entry Point

The entry point is the first node(s) that are run when the graph starts. You can use the addEdge method from the virtual START node to the first node to execute to specify where to enter the graph.

import static org.bsc.langgraph4j.StateGraph.START;

graph.addEdge(START, "nodeA");

Conditional Entry Point

A conditional entry point lets you start at different nodes depending on custom logic. You can use addConditionalEdges from the virtual START node to accomplish this.

import static org.bsc.langgraph4j.StateGraph.START;
import static org.bsc.langgraph4j.utils.CollectionsUtils.mapOf;

graph.addConditionalEdges(START, routingFunction, 
      EdgeMapping.builder()
          .to( "nodeB", "first")
          .to( "nodeC", "second" )
          .build());

You must provide an object that maps the routingFunction's output to the name of the next node.

Graph execution context persistence

Checkpoint Saver

LangGraph4j has a built-in persistence layer, implemented through CheckpointSaver. When you use a CheckpointerSaver with a graph, you can interact with the state of that graph. The checkpointerSaver saves a checkpoint of the graph state at every step, enabling several powerful capabilities:

First, checkpoints facilitate human-in-the-loop workflows by allowing humans to inspect, interrupt, and approve steps. CheckpointerSaver is need for these workflows as the human has to be able to view the state of a graph at any point in time, and the graph has to be to resume execution after the human has made any updates to the state.

Second, it allows for "memory" between interactions. You can use checkpointSaver to create threads and save the state of a thread after a graph executes. In the case of repeated human interactions (like conversations) any follow up messages can be sent to that checkpoint, which will retain its memory of previous ones.

See this guide for how to add a CheckpointSaver to your graph.

Threads

Threads enable the checkpointing of multiple different runs, making them essential for multi-tenant chat applications and other scenarios where maintaining separate states is necessary. A thread is a unique session ID assigned to a series of checkpoints saved by a ChechpointSaver so when using a checkpointer, you must specify a thread_id when running the graph.

You must pass these when invoking the graph as part of the configurable part of the config.

var config = RunnableConfig.builder()
                                  .threadId("<unique execute session id>")
                                  .build();
graph.invoke(inputs, config);

See this guide for how to use threads.

👉 Thread ID Uniqueness & Checkpoint Persistence

When a persistent CheckpointSaver is in use, the thread_id acts as the primary key of the checkpoint storage so keep the following in mind:

A thread_id identifies one conversation timeline: reuse the same id to continue that conversation, and generate a fresh one to start a new conversation. Generate ids programmatically, e.g. UUID.randomUUID(), optionally prefixed with a tenant/session qualifier (--) when several clients share the same storage. Reusing a thread_id that already has checkpoints does not start a clean conversation: persistent savers will reject the conflicting insert (typically a primary-key violation). Note that UI tooling (such as LangGraph Studio) may reuse default thread names like default or thread_1 across restarts: when running against a persistent saver, always provide a freshly generated thread_id in your RunnableConfig.

Interruptions

In langgraph4j, a graph's execution can be paused at any node. This is particularly useful for implementing features like human-in-the-loop approvals, where the graph needs to wait for external input before proceeding.

To set interruptions before or after certain nodes execute. This can be used to wait for human approval before continuing. These can be set when you "compile" a graph.

Static definition

You can set Interruptions either before a node executes (using interruptBefore) or after a node executes (using interruptAfter) adding them on CompileConfig.

var compileConfig = CompileConfig.builder()
                    .checkpointSaver(saver)
                    .interruptBefore( "tools")
                    .build();

Dynamic definition

The org.bsc.langgraph4j.action.InterruptibleAction<State> interface is the core component that enables this functionality. Any node action that implements this interface can conditionally interrupt the graph's execution.

The heart of the interface is the interrupt method:

public interface InterruptibleAction<State extends AgentState> {
   /**
    * Determines whether the graph execution should be interrupted at the current node.
    *
    * @param nodeId The identifier of the current node being processed.
    * @param state  The current state of the agent.
    * @return An {@link Optional} containing {@link InterruptionMetadata} if the execution
    *         should be interrupted. Returns an empty {@link Optional} to continue execution.
   */
   Optional<InterruptionMetadata<State>> interrupt(String nodeId, State state, RunnableConfig config );
}

Here’s how it works:

  • When the graph is about to execute a node, it first checks if the node's action implements InterruptibleAction.
  • If it does, the interrupt(String nodeId, State state) method is called.
  • If the method returns a non-empty Optional, the graph's execution is paused. The InterruptionMetadata object contains information about the interruption, which can be sent to an external system or user for review.
  • If the method returns an empty Optional, the node executes normally, and the graph continues its execution without interruption.

Use the GraphInterruptException

GraphInterruptException is a built-in exception that a node action can raise when it needs to stop graph execution intentionally. The runtime does not treat this exception as a graph error. Instead, it converts the exception into an InterruptionMetadata result so the execution can be inspected and resumed later.

This is useful when the decision to interrupt happens inside the node body, for example after validating external input, detecting that user approval is required, or receiving a response that must be reviewed before the graph continues.

// From a sync NodeAction
public Map<String, Object> apply(State state, RunnableConfig config) throws Exception {
    throw new GraphInterruptException(config, "Interrupting with exception!");
}

// From an async NodeAction
public CompletableFuture<Map<String, Object>> apply(State state, RunnableConfig config) {
    return failedFuture(new GraphInterruptException(config, "Interrupting with exception!"));
}

Here’s how it works:

  • The graph engine executes the current node action.
  • If the action throws an exception, or returns a failed CompletableFuture, the engine checks the cause.
  • If no GraphInterruptException is found, the exception is handled as a normal execution error and the stream completes with an error.
  • If a GraphInterruptException is found, the engine resolves the interrupted node id.
  • The engine builds an InterruptionMetadata object with the node id, the state, and the exception message as the interruption reason.
  • The stream is completed, so the interruption is available as the final graph result instead of an error.
  • The graph execution loop stops. You can then read the Interruption Metadata from the stream result and resume the graph using Graph Input.resume() or GraphInput.resume(Map) as usual.

You MUST use a checkpoiner when using interruption. This is because your graph needs to be able to resume execution.

In order to resume execution, you can just invoke your graph with GraphInput.resume() or GraphInput.resume(Map) as the input.

// Initial run of graph
graph.stream(inputs, config);

// Let's assume it hit an interruption somewhere, you can then resume it no passing new state data
graph.stream(GraphInput.resume(), config);

// Let's assume it hit an interruption somewhere, you can then resume it passing new state data
graph.stream(GraphInput.resume( Map.of( "key", "value")), config);

Achieve InterruptionMetadata object after interruption

It is most important understand that the nodes iterator holds the final result of graph execution. In the case of interruption the InterruptionMetadata instance will be set as iterator's result so you can achieve it using : GraphResult as shown below

var generator = app.stream( inputs );
for (var step : iterator) {
   ...
}

var finalResult =  GraphResult.from(generator);

if( finalResult.isInterruptionMetadata()) {
    var interruptionMetadata = finalResult.asInterruptionMetadata();

    ....
}

resultValue is a generic Object that in case of interruptions is an instance of InterruptionMetadata

See Wait for user Input (HITL) for a full walkthrough of how to add interruptions.

Visualization

It's often nice to be able to visualize graphs, especially as they get more complex. LangGraph4j comes with several built-in ways to visualize graphs using diagram-as-code tools such as [PlantUML] and Mermaid through the graph.getGraph method.

// for PlantUML
GraphRepresentation result = app.getGraph(GraphRepresentation.Type.PLANTUML);

System.out.println(result.getContent());

// for Mermaid
GraphRepresentation result = app.getGraph(GraphRepresentation.Type.MERMAID);
System.out.println(result.getContent());