MySQL Checkpoint Saver V2
Version 2 is implemented by org.bsc.langgraph4j.checkpoint.MySQLSaverV2. It extends the original MySQL saver model with release tags, error metadata, and interruption state.
Use V2 for new applications unless you must keep compatibility with an existing V1 schema.
Data Architecture
The schema is defined in src/main/resources/db/migration/v2.0__init.sql.
erDiagram
LG4JThread {
BIGINT thread_id PK
VARCHAR thread_name UK
BIGINT parent_thread_id FK
BOOLEAN is_interrupted
TEXT message
TIMESTAMP created_at
}
LG4JThreadTag {
BIGINT thread_id PK
VARCHAR thread_name
INTEGER released_version
BIGINT parent_thread_id
BOOLEAN is_released
BOOLEAN is_error
TEXT message
TIMESTAMP created_at
}
LG4JCheckpoint {
VARCHAR checkpoint_id PK
VARCHAR parent_checkpoint_id
BIGINT thread_id
VARCHAR node_id
VARCHAR next_node_id
TEXT state_data
VARCHAR state_content_type
TIMESTAMP saved_at
}
LG4JThread ||--o{ LG4JCheckpoint : owns_live
LG4JThreadTag ||--o{ LG4JCheckpoint : owns_released
LG4JCheckpoint.thread_id stores the row id from LG4JThread. The migration does not define a foreign key from checkpoints to live threads so released checkpoints can continue to reference the archived row id after the live thread is deleted.
Design
V2 keeps only active executions in LG4JThread. The row id is a MySQL auto-increment BIGINT, and thread_name is unique.
When a checkpoint is saved, the saver:
- Inserts or refreshes the live
LG4JThreadrow for the logical thread name. - Obtains the
thread_idwith MySQLLAST_INSERT_ID(). - Inserts an
LG4JCheckpointrow that references that identity value.
When the thread is released, the saver:
- Copies the live thread row into
LG4JThreadTag. - Assigns the next
released_versionfor the samethread_name. - Records release status, error status, optional message, and original creation time.
- Deletes the live row from
LG4JThread.
The checkpoint rows remain associated with the same numeric thread_id, which is now represented by LG4JThreadTag.thread_id.
State Serialization
It serializes state through the configured StateSerializer, encoding the serializer bytes as Base64 text. state_content_type records the serializer content type so the saver can select the correct registered serializer while loading checkpoints or tags.
Release Tags
MySQLSaverV2.tag(config, version) loads checkpoints for a released version from LG4JThreadTag.
var config = RunnableConfig.builder()
.threadId("customer-support-thread")
.build();
var releasedVersion = saver.tag(config, 1);
Interruption Metadata
registerInterruption(...) updates the active LG4JThread row by setting is_interrupted = TRUE and storing the interruption reason in message. This lets external tools or dashboards see that a live thread is waiting for intervention.
Dashboard Queries
MySQLSaverV2Dashboard is a read-only companion for an existing V2 schema. It exposes selectAllThreads() and selectAllTags(); it does not initialize tables or write checkpoints, releases, or interruptions.
var dashboard = MySQLSaverV2Dashboard.builder()
.dataSource(dataSource)
.stateSerializer(new ObjectStreamStateSerializer<>(AgentState::new))
.build();
var activeThreads = dashboard.selectAllThreads();
var releasedTags = dashboard.selectAllTags();
Limitations
- V2 is not schema-compatible with V1. Existing V1 tables require an explicit migration plan before switching classes.
LG4JCheckpoint.thread_idhas no foreign-key constraint in the bundled migration. This is intentional for archived tags, but database cleanup must account for it.thread_nameis unique among live threads, so concurrent executions with the same logical thread name share the same active row.- Released checkpoints are loaded through
tag(...); normal checkpoint loading reads the live thread table. released_versionis assigned by querying the existing maximum for the thread name. Coordinate concurrent releases of the same thread if strict version sequencing is required.
Build a Saver
import com.mysql.cj.jdbc.MysqlDataSource;
import org.bsc.langgraph4j.checkpoint.CreateOption;
import org.bsc.langgraph4j.checkpoint.MySQLSaverV2;
import org.bsc.langgraph4j.serializer.std.ObjectStreamStateSerializer;
import org.bsc.langgraph4j.state.AgentState;
var dataSource = new MysqlDataSource();
dataSource.setURL("jdbc:mysql://localhost:3306/langgraph4j");
dataSource.setUser("app_user");
dataSource.setPassword("secret");
var saver = MySQLSaverV2.builder()
.dataSource(dataSource)
.stateSerializer(new ObjectStreamStateSerializer<>(AgentState::new))
.createOption(CreateOption.CREATE_IF_NOT_EXISTS)
.build();
Use the saver when compiling a graph:
var compileConfig = CompileConfig.builder()
.checkpointSaver(saver)
.build();
var workflow = graph.compile(compileConfig);
Migration Resource
For managed database migrations, apply:
src/main/resources/db/migration/v2.0__init.sql
The saver uses this same resource when createOption(CreateOption.CREATE_IF_NOT_EXISTS) is enabled.