Configure state persistence

By default, Quarkus Flow executes workflows entirely in memory. While this is incredibly fast, it means that if your application crashes, restarts, or is scaled down, any currently running workflow instances are lost.

State Persistence solves this by writing the workflow’s state to a durable datastore every time a task completes or pauses.

Enabling persistence allows your workflows to survive:

  • Asynchronous Callbacks: Pausing a workflow for days or weeks while waiting for a human approval (HITL) or an external webhook.

  • Failures & Restarts: Recovering automatically from unexpected JVM crashes.

  • Kubernetes Node Drains: Safely migrating running workflows across pods during rolling updates, deployments, or cluster scaling events.

(For a deeper conceptual dive into how this enables cluster-wide orchestration, see Durable Workflows in Kubernetes).

The Quarkus Flow team is currently actively testing and benchmarking these persistence providers for high-throughput production scenarios. Expect deeper performance tuning guides in the future.

1. Automatic Restoration

When a persistence provider is active, Quarkus Flow will automatically attempt to resume interrupted workflows upon application startup.

When the server boots, any workflow instance that was running when the JVM previously shut down (as identified by its application ID) will resume execution from its last recorded checkpoint.

If you need to disable this auto-resume behavior, add this to your application.properties:

quarkus.flow.persistence.autoRestore=false

2. Excluding Workflows from Persistence

In some scenarios, you may want to exclude specific workflows from persistence while keeping persistence enabled for others.

To exclude workflows from persistence, add their IDs in namespace:name:version format to the exclusion list in your application.properties:

quarkus.flow.persistence.exclude-workflows=com.example:temporary-workflow:1.0.0,com.example:test-workflow:0.1.0

Excluded workflows will:

  • Execute entirely in-memory

  • Not be persisted to the datastore

  • Not be restored after application restart

  • Lose all state if the application crashes or restarts

3. Choose a Persistence Provider

Quarkus Flow provides three drop-in persistence implementations. You should only include one of these dependencies in your project at a time.

Redis is an in-memory data structure store that provides excellent performance for high-throughput workflow checkpoints.

pom.xml
<dependency>
  <groupId>io.quarkiverse.flow</groupId>
  <artifactId>quarkus-flow-redis</artifactId>
</dependency>

Configure the Redis connection in your application.properties:

quarkus.redis.hosts=redis://localhost:6379
Our integration tests use Valkey image instead.

Using Infinispan Server RESP

Quarkus Flow can use Infinispan Server as a Redis-compatible persistence backend through the Infinispan RESP endpoint.

This mode still uses the quarkus-flow-redis persistence implementation and the Quarkus Redis client. No Quarkus Flow Infinispan-specific runtime dependency is required.

Infinispan Server exposes RESP on the default single-port endpoint. Redis clients connect to the same server port, 11222, and Infinispan routes RESP traffic to the RESP connector.

<dependency>
  <groupId>io.quarkiverse.flow</groupId>
  <artifactId>quarkus-flow-redis</artifactId>
</dependency>

Configure the Quarkus Redis client to point to the Infinispan Server RESP endpoint:

quarkus.redis.hosts=redis://admin:password@infinispan-server:11222

For local development with Docker:

docker run --rm \
  --name infinispan \
  -p 11222:11222 \
  -e USER=admin \
  -e PASS=password \
  quay.io/infinispan/server:15.0

Then configure your application:

quarkus.redis.hosts=redis://admin:password@localhost:11222

For Kubernetes deployments managed by the Infinispan Operator, point quarkus.redis.hosts to the internal Infinispan service name:

quarkus.redis.hosts=redis://admin:password@infinispan:11222

A minimal Infinispan cluster managed by the Operator can be created with:

apiVersion: infinispan.org/v1
kind: Infinispan
metadata:
  name: infinispan
spec:
  replicas: 2

Use Kubernetes Secret or your platform secret-management mechanism to provide the username and password to the Quarkus application instead of hardcoding credentials in application.properties.

For example:

quarkus.redis.hosts=redis://${INFINISPAN_USER}:${INFINISPAN_PASSWORD}@infinispan:11222

Option B: JPA (Relational Databases)

JPA allows you to store workflow state in a standard relational database (e.g., PostgreSQL, MySQL). This is ideal if your enterprise architecture already relies heavily on a relational database.

pom.xml
<dependency>
  <groupId>io.quarkiverse.flow</groupId>
  <artifactId>quarkus-flow-jpa</artifactId>
</dependency>

You must also configure a Quarkus JDBC driver and datasource. See the Quarkus Datasources Guide for full instructions.

Database Schema Migration

Quarkus Flow can create the persistence tables automatically through the Quarkus Hibernate ORM schema management configuration:

quarkus.hibernate-orm.database.generation=update

This is convenient for development and testing. For production, prefer managing the schema explicitly with a migration tool such as Flyway.

Add the Flyway extension (and the matching database support artifact, if required for your database):

pom.xml
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-flyway</artifactId>
</dependency>

Enable migration at startup and disable Hibernate ORM schema generation in your application.properties:

quarkus.hibernate-orm.database.generation=none
quarkus.flyway.enabled=true
quarkus.flyway.migrate-at-start=true

Then create a migration script at src/main/resources/db/migration/V1__create_tables.sql with the three tables required by the JPA provider:

+ H2
CREATE TABLE cloud_event_entity
(
    id                VARCHAR(255) NOT NULL,
    reg_id            VARCHAR(255) NOT NULL,
    source            VARCHAR(255) NOT NULL,
    type              VARCHAR(255) NOT NULL,
    subject           VARCHAR(255),
    data_content_type VARCHAR(255),
    data_schema       VARCHAR(255),
    time              TIMESTAMP(6) WITH TIME ZONE,
    data              VARBINARY,
    extensions        VARBINARY,
    processed_flag    BOOLEAN DEFAULT FALSE,
    version           TINYINT      NOT NULL CHECK (version BETWEEN 0 AND 1),
    PRIMARY KEY (id)
);

CREATE TABLE process_instance_entity
(
    application_id     VARCHAR(255)                NOT NULL,
    instance_id        VARCHAR(255)                NOT NULL,
    workflow_name      VARCHAR(255)                NOT NULL,
    workflow_namespace VARCHAR(255)                NOT NULL,
    workflow_version   VARCHAR(255)                NOT NULL,
    started_at         TIMESTAMP(6) WITH TIME ZONE NOT NULL,
    status             TINYINT CHECK (status BETWEEN 0 AND 6),
    input              VARBINARY,
    PRIMARY KEY (application_id, instance_id)
);

CREATE TABLE task_info_entity
(
    application_id      VARCHAR(255) NOT NULL,
    process_instance_id VARCHAR(255) NOT NULL,
    json_pointer        VARCHAR(255) NOT NULL,
    iteration           INTEGER      NOT NULL,
    task_type           INTEGER      NOT NULL CHECK (task_type IN (1, 2)),
    is_end_node         BOOLEAN,
    retry_attempt       SMALLINT,
    instant             TIMESTAMP(6) WITH TIME ZONE,
    next_position       VARCHAR(255),
    context             VARBINARY,
    model               VARBINARY,
    PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
    CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
    CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
    CONSTRAINT fk_task_process_instance
        FOREIGN KEY (application_id, process_instance_id)
            REFERENCES process_instance_entity (application_id, instance_id)
);
+ MySQL
CREATE TABLE cloud_event_entity
(
    id                VARCHAR(255) NOT NULL,
    reg_id            VARCHAR(255) NOT NULL,
    source            VARCHAR(255) NOT NULL,
    type              VARCHAR(255) NOT NULL,
    subject           VARCHAR(255),
    data_content_type VARCHAR(255),
    data_schema       VARCHAR(255),
    `time`            DATETIME(6),
    data              LONGBLOB,
    extensions        LONGBLOB,
    processed_flag    BOOLEAN DEFAULT FALSE,
    version           TINYINT      NOT NULL CHECK (version BETWEEN 0 AND 1),
    PRIMARY KEY (id)
);

CREATE TABLE process_instance_entity
(
    application_id     VARCHAR(255) NOT NULL,
    instance_id        VARCHAR(255) NOT NULL,
    workflow_name      VARCHAR(255) NOT NULL,
    workflow_namespace VARCHAR(255) NOT NULL,
    workflow_version   VARCHAR(255) NOT NULL,
    started_at         DATETIME(6)  NOT NULL,
    status             TINYINT CHECK (status BETWEEN 0 AND 6),
    input              LONGBLOB,
    PRIMARY KEY (application_id, instance_id)
);

CREATE TABLE task_info_entity
(
    application_id      VARCHAR(255) NOT NULL,
    process_instance_id VARCHAR(255) NOT NULL,
    json_pointer        VARCHAR(255) NOT NULL,
    iteration           INTEGER      NOT NULL,
    task_type           INTEGER      NOT NULL CHECK (task_type IN (1, 2)),
    is_end_node         BOOLEAN,
    retry_attempt       SMALLINT,
    instant             DATETIME(6),
    next_position       VARCHAR(255),
    context             LONGBLOB,
    model               LONGBLOB,
    PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
    CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
    CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
    CONSTRAINT fk_task_process_instance
        FOREIGN KEY (application_id, process_instance_id)
            REFERENCES process_instance_entity (application_id, instance_id)
);
+ PostgreSQL
CREATE TABLE cloud_event_entity
(
    id                VARCHAR(255) NOT NULL,
    reg_id            VARCHAR(255) NOT NULL,
    source            VARCHAR(255) NOT NULL,
    type              VARCHAR(255) NOT NULL,
    subject           VARCHAR(255),
    data_content_type VARCHAR(255),
    data_schema       VARCHAR(255),
    time              TIMESTAMP(6) WITH TIME ZONE,
    data              BYTEA,
    extensions        BYTEA,
    processed_flag    BOOLEAN DEFAULT FALSE,
    version           SMALLINT     NOT NULL CHECK (version BETWEEN 0 AND 1),
    PRIMARY KEY (id)
);

CREATE TABLE process_instance_entity
(
    application_id     VARCHAR(255)                NOT NULL,
    instance_id        VARCHAR(255)                NOT NULL,
    workflow_name      VARCHAR(255)                NOT NULL,
    workflow_namespace VARCHAR(255)                NOT NULL,
    workflow_version   VARCHAR(255)                NOT NULL,
    started_at         TIMESTAMP(6) WITH TIME ZONE NOT NULL,
    status             SMALLINT CHECK (status BETWEEN 0 AND 6),
    input              BYTEA,
    PRIMARY KEY (application_id, instance_id)
);

CREATE TABLE task_info_entity
(
    application_id      VARCHAR(255) NOT NULL,
    process_instance_id VARCHAR(255) NOT NULL,
    json_pointer        VARCHAR(255) NOT NULL,
    iteration           INTEGER      NOT NULL,
    task_type           INTEGER      NOT NULL CHECK (task_type IN (1, 2)),
    is_end_node         BOOLEAN,
    retry_attempt       SMALLINT,
    instant             TIMESTAMP(6) WITH TIME ZONE,
    next_position       VARCHAR(255),
    context             BYTEA,
    model               BYTEA,
    PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
    CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
    CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
    CONSTRAINT fk_task_process_instance
        FOREIGN KEY (application_id, process_instance_id)
            REFERENCES process_instance_entity (application_id, instance_id)
);
+ Oracle
CREATE TABLE cloud_event_entity
(
    id                VARCHAR2(255) NOT NULL,
    reg_id            VARCHAR2(255) NOT NULL,
    source            VARCHAR2(255) NOT NULL,
    type              VARCHAR2(255) NOT NULL,
    subject           VARCHAR2(255),
    data_content_type VARCHAR2(255),
    data_schema       VARCHAR2(255),
    time              TIMESTAMP(6) WITH TIME ZONE,
    data              BLOB,
    extensions        BLOB,
    processed_flag    NUMBER(1, 0) DEFAULT 0,
    version           NUMBER(3, 0)  NOT NULL CHECK (version BETWEEN 0 AND 1),
    PRIMARY KEY (id)
);

CREATE TABLE process_instance_entity
(
    application_id     VARCHAR2(255)               NOT NULL,
    instance_id        VARCHAR2(255)               NOT NULL,
    workflow_name      VARCHAR2(255)               NOT NULL,
    workflow_namespace VARCHAR2(255)               NOT NULL,
    workflow_version   VARCHAR2(255)               NOT NULL,
    started_at         TIMESTAMP(6) WITH TIME ZONE NOT NULL,
    status             NUMBER(3, 0) CHECK (status BETWEEN 0 AND 6),
    input              BLOB,
    PRIMARY KEY (application_id, instance_id)
);

CREATE TABLE task_info_entity
(
    application_id      VARCHAR2(255) NOT NULL,
    process_instance_id VARCHAR2(255) NOT NULL,
    json_pointer        VARCHAR2(255) NOT NULL,
    iteration           NUMBER(10, 0) NOT NULL,
    task_type           NUMBER(10, 0) NOT NULL CHECK (task_type IN (1, 2)),
    is_end_node         NUMBER(1, 0),
    retry_attempt       NUMBER(5, 0),
    instant             TIMESTAMP(6) WITH TIME ZONE,
    next_position       VARCHAR2(255),
    context             BLOB,
    model               BLOB,
    PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
    CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
    CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
    CONSTRAINT fk_task_process_instance
        FOREIGN KEY (application_id, process_instance_id)
            REFERENCES process_instance_entity (application_id, instance_id)
);
+ MSSQL
CREATE TABLE cloud_event_entity
(
    id                VARCHAR(255) NOT NULL,
    reg_id            VARCHAR(255) NOT NULL,
    source            VARCHAR(255) NOT NULL,
    type              VARCHAR(255) NOT NULL,
    subject           VARCHAR(255),
    data_content_type VARCHAR(255),
    data_schema       VARCHAR(255),
    [time]            DATETIMEOFFSET(6),
    data              VARBINARY(MAX),
    extensions        VARBINARY(MAX),
    processed_flag    BIT DEFAULT 0,
    version           TINYINT      NOT NULL CHECK (version BETWEEN 0 AND 1),
    PRIMARY KEY (id)
    );

CREATE TABLE process_instance_entity
(
    application_id     VARCHAR(255)      NOT NULL,
    instance_id        VARCHAR(255)      NOT NULL,
    workflow_name      VARCHAR(255)      NOT NULL,
    workflow_namespace VARCHAR(255)      NOT NULL,
    workflow_version   VARCHAR(255)      NOT NULL,
    started_at         DATETIMEOFFSET(6) NOT NULL,
    status             TINYINT CHECK (status BETWEEN 0 AND 6),
    input              VARBINARY(MAX),
    PRIMARY KEY (application_id, instance_id)
);

CREATE TABLE task_info_entity
(
    application_id      VARCHAR(255) NOT NULL,
    process_instance_id VARCHAR(255) NOT NULL,
    json_pointer        VARCHAR(255) NOT NULL,
    iteration           INT          NOT NULL,
    task_type           INT          NOT NULL CHECK (task_type IN (1, 2)),
    is_end_node         BIT,
    retry_attempt       SMALLINT,
    instant             DATETIMEOFFSET(6),
    next_position       VARCHAR(255),
    context             VARBINARY(MAX),
    model               VARBINARY(MAX),
    PRIMARY KEY (iteration, application_id, json_pointer, process_instance_id),
    CHECK (task_type <> 1 OR (is_end_node IS NOT NULL)),
    CHECK (task_type <> 2 OR (retry_attempt IS NOT NULL)),
    CONSTRAINT fk_task_process_instance
        FOREIGN KEY (application_id, process_instance_id)
            REFERENCES process_instance_entity (application_id, instance_id)
);

Each tab provides the migration script for a specific database. Pick the tab that matches your target database and adapt it further if your schema or naming conventions differ. Note that the time column is a reserved word in some databases, so it is quoted on MySQL ( time ) and MSSQL ([time]).

Option C: MVStore (Local file system)

MVStore writes workflow state to a local file system file. This is an excellent, zero-infrastructure option for local development, testing, or single-node edge deployments.

pom.xml
<dependency>
  <groupId>io.quarkiverse.flow</groupId>
  <artifactId>quarkus-flow-mvstore</artifactId>
</dependency>

Configure the local file path in your application.properties:

quarkus.flow.persistence.mvstore.db-path=${user.home}/testFlowMVStore.db

See also