diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java b/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java
index f893dfeec5..38de7e6241 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java
@@ -13,9 +13,13 @@
package io.dapr.durabletask;
+import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent;
+import io.dapr.durabletask.implementation.protobuf.OrchestratorService;
+
import javax.annotation.Nullable;
import java.time.Duration;
+import java.util.List;
import java.util.concurrent.TimeoutException;
/**
@@ -445,6 +449,39 @@ public String restartInstance(
return this.restartInstance(instanceId, restartWithNewInstanceId);
}
+ /**
+ * Resumes a running orchestration instance.
+ *
+ * @param instanceId the ID of the orchestration instance to resume
+ * @param reason the reason for resuming the orchestration instance
+ */
+ public abstract void resumeInstance(String instanceId, @Nullable String reason);
+
+ /**
+ * Resumes a running orchestration instance.
+ *
+ * @param instanceId the ID of the orchestration instance to resume
+ */
+ public void resumeInstance(String instanceId) {
+ this.resumeInstance(instanceId, null);
+ }
+
+ /**
+ * Resumes a running orchestration instance owned by another app.
+ *
+ *
Requires a Dapr runtime with cross-app workflow support; against an older runtime the target app ID is
+ * ignored and the instance is resumed on the local app instead.
+ *
+ * @param instanceId the ID of the orchestration instance to resume
+ * @param reason the reason for resuming the orchestration instance
+ * @param appID the ID of the app that owns the target orchestration instance, used for cross-app
+ * routing. May be null to target the local app.
+ */
+ public void resumeInstance(String instanceId, @Nullable String reason, @Nullable String appID) {
+ requireLocalRouting(appID);
+ this.resumeInstance(instanceId, reason);
+ }
+
/**
* Suspends a running orchestration instance.
*
@@ -479,37 +516,35 @@ public void suspendInstance(String instanceId, @Nullable String reason, @Nullabl
}
/**
- * Resumes a running orchestration instance.
+ * Lists workflow instance IDs with optional pagination.
*
- * @param instanceId the ID of the orchestration instance to resume
+ * @param continuationToken the continuation token from a previous call, or null for the first page
+ * @param pageSize the maximum number of instance IDs to return, or null for no limit
+ * @return the raw list-instance-IDs response from the sidecar
*/
- public void resumeInstance(String instanceId) {
- this.resumeInstance(instanceId, null);
- }
+ public abstract OrchestratorService.ListInstanceIDsResponse listInstanceIds(
+ @Nullable String continuationToken, @Nullable Integer pageSize);
/**
- * Resumes a running orchestration instance.
+ * Gets the full execution history of a workflow instance.
*
- * @param instanceId the ID of the orchestration instance to resume
- * @param reason the reason for resuming the orchestration instance
+ * @param instanceId the ID of the workflow instance to get history for
+ * @return the list of history events for the workflow instance
*/
- public abstract void resumeInstance(String instanceId, @Nullable String reason);
+ public abstract List getInstanceHistory(String instanceId);
/**
- * Resumes a running orchestration instance owned by another app.
- *
- * Requires a Dapr runtime with cross-app workflow support; against an older runtime the target app ID is
- * ignored and the instance is resumed on the local app instead.
- *
- * @param instanceId the ID of the orchestration instance to resume
- * @param reason the reason for resuming the orchestration instance
- * @param appID the ID of the app that owns the target orchestration instance, used for cross-app
- * routing. May be null to target the local app.
+ * Reruns a workflow from a specific history event, creating a new workflow instance.
+ *
+ * @param sourceInstanceId the ID of the source workflow instance to rerun from
+ * @param eventId the history event ID to rerun from
+ * @param newInstanceId the instance ID to use for the new instance, or null for a random ID
+ * @param input the input applied at the next activity event, used only when overwriteInput is true
+ * @param overwriteInput true to overwrite the input at the rerun point with input
+ * @return the instance ID of the new workflow instance
*/
- public void resumeInstance(String instanceId, @Nullable String reason, @Nullable String appID) {
- requireLocalRouting(appID);
- this.resumeInstance(instanceId, reason);
- }
+ public abstract String rerunWorkflowFromEvent(String sourceInstanceId, int eventId,
+ @Nullable String newInstanceId, @Nullable Object input, boolean overwriteInput);
/**
* Rejects a cross-app target on a client that does not implement cross-app routing. Implementations that
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java b/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java
index c572dfe76b..44f9dff348 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java
@@ -15,6 +15,7 @@
import com.google.protobuf.StringValue;
import com.google.protobuf.Timestamp;
+import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent;
import io.dapr.durabletask.implementation.protobuf.Orchestration;
import io.dapr.durabletask.implementation.protobuf.OrchestratorService;
import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc;
@@ -45,6 +46,7 @@
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@@ -551,6 +553,57 @@ public String restartInstance(String instanceId, boolean restartWithNewInstanceI
return this.scheduleNewOrchestrationInstance(metadata.getName(), options);
}
+ @Override
+ public OrchestratorService.ListInstanceIDsResponse listInstanceIds(
+ @Nullable String continuationToken, @Nullable Integer pageSize) {
+ OrchestratorService.ListInstanceIDsRequest.Builder builder =
+ OrchestratorService.ListInstanceIDsRequest.newBuilder();
+ if (continuationToken != null) {
+ builder.setContinuationToken(continuationToken);
+ }
+ if (pageSize != null) {
+ if (pageSize <= 0) {
+ throw new IllegalArgumentException("pageSize must be greater than zero.");
+ }
+ builder.setPageSize(pageSize);
+ }
+ return this.sidecarClient.listInstanceIDs(builder.build());
+ }
+
+ @Override
+ public List getInstanceHistory(String instanceId) {
+ Helpers.throwIfArgumentNull(instanceId, "instanceId");
+ OrchestratorService.GetInstanceHistoryRequest request =
+ OrchestratorService.GetInstanceHistoryRequest.newBuilder()
+ .setInstanceId(instanceId)
+ .build();
+ OrchestratorService.GetInstanceHistoryResponse response = this.sidecarClient.getInstanceHistory(request);
+ return response.getEventsList();
+ }
+
+ @Override
+ public String rerunWorkflowFromEvent(String sourceInstanceId, int eventId,
+ @Nullable String newInstanceId, @Nullable Object input, boolean overwriteInput) {
+ Helpers.throwIfArgumentNull(sourceInstanceId, "sourceInstanceId");
+ OrchestratorService.RerunWorkflowFromEventRequest.Builder builder =
+ OrchestratorService.RerunWorkflowFromEventRequest.newBuilder()
+ .setSourceInstanceID(sourceInstanceId)
+ .setEventID(eventId)
+ .setOverwriteInput(overwriteInput);
+ if (newInstanceId != null) {
+ builder.setNewInstanceID(newInstanceId);
+ }
+ if (overwriteInput) {
+ String serializedInput = this.dataConverter.serialize(input);
+ if (serializedInput != null) {
+ builder.setInput(StringValue.of(serializedInput));
+ }
+ }
+ OrchestratorService.RerunWorkflowFromEventResponse response =
+ this.sidecarClient.rerunWorkflowFromEvent(builder.build());
+ return response.getNewInstanceID();
+ }
+
private PurgeResult toPurgeResult(OrchestratorService.PurgeInstancesResponse response) {
return new PurgeResult(response.getDeletedInstanceCount());
}
diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java b/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java
index 6c870b8452..2a7b389ddc 100644
--- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java
+++ b/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java
@@ -1745,7 +1745,6 @@ public void taskExecutionIdTest() {
}
}
-
}
diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md
index 167a423d5d..f93f5c2b39 100644
--- a/examples/src/main/java/io/dapr/examples/workflows/README.md
+++ b/examples/src/main/java/io/dapr/examples/workflows/README.md
@@ -984,4 +984,55 @@ The client log:
```text
Started a new external-event model workflow with instance ID: 23410d96-1afe-4698-9fcd-c01c1e0db255
workflow instance with ID: 23410d96-1afe-4698-9fcd-c01c1e0db255 completed.
-```
\ No newline at end of file
+```
+
+### Workflow Management (List, History, Rerun) Pattern
+
+The `DaprWorkflowClient` can list workflow instance IDs, read a workflow instance's full
+execution history, and rerun a workflow from a specific history event. This example shows
+all three operations.
+
+
+
+Run the worker:
+
+```sh
+dapr run --app-id demoworkflowworker --resources-path ./components/workflows --dapr-grpc-port 50003 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.management.DemoWorkflowManagementWorker 50003
+```
+
+
+
+
+
+In a separate terminal, run the client. It connects to the worker's sidecar on
+gRPC port 50003, so the workflow it schedules runs on the worker:
+
+```sh
+java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.management.DemoWorkflowManagementClient 50003
+dapr stop --app-id demoworkflowworker
+```
+
+
+
+The client output shows the started instance ID, the completed result, the list of history
+events (each with its event ID, type, and timestamp), the new instance ID from the rerun,
+and the count of listed instance IDs.
\ No newline at end of file
diff --git a/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementActivity.java b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementActivity.java
new file mode 100644
index 0000000000..7db7d116ae
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementActivity.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.examples.workflows.management;
+
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DemoWorkflowManagementActivity implements WorkflowActivity {
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Logger logger = LoggerFactory.getLogger(DemoWorkflowManagementActivity.class);
+ logger.info("Starting Activity: " + ctx.getName());
+ String message = ctx.getInput(String.class);
+ String newMessage = message.toUpperCase();
+ logger.info("Message Received from input: " + message);
+ return newMessage;
+ }
+}
diff --git a/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementClient.java b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementClient.java
new file mode 100644
index 0000000000..d1a811f4d3
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementClient.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.examples.workflows.management;
+
+import io.dapr.examples.workflows.utils.PropertyUtils;
+import io.dapr.workflows.client.DaprWorkflowClient;
+import io.dapr.workflows.client.RerunWorkflowFromEventOptions;
+import io.dapr.workflows.client.WorkflowHistoryEvent;
+import io.dapr.workflows.client.WorkflowInstancePage;
+import io.dapr.workflows.client.WorkflowState;
+
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+public class DemoWorkflowManagementClient {
+ /**
+ * The main method to start the client.
+ *
+ * @param args Input arguments (unused).
+ */
+ public static void main(String[] args) {
+ try (DaprWorkflowClient client = new DaprWorkflowClient(PropertyUtils.getProperties(args))) {
+ String instanceId = client.scheduleNewWorkflow(DemoWorkflowManagementWorkflow.class);
+ System.out.printf("Started a new workflow with instance ID: %s%n", instanceId);
+
+ WorkflowState state = client.waitForWorkflowCompletion(instanceId, null, true);
+ System.out.printf("Workflow completed with result: %s%n", state.readOutputAs(String.class));
+
+ // Read the full execution history.
+ List history = client.getInstanceHistory(instanceId);
+ System.out.printf("History for %s has %d events:%n", instanceId, history.size());
+ for (WorkflowHistoryEvent event : history) {
+ System.out.printf(" eventId=%d type=%s at=%s%n",
+ event.getEventId(), event.getEventType(), event.getTimestamp());
+ }
+
+ // Rerun the workflow from the activity's scheduled event. Lifecycle events
+ // (for example WORKFLOW_STARTED) carry event ID -1 and cannot be rerun; only
+ // events with a non-negative ID, such as a scheduled activity, are valid targets.
+ int rerunEventId = history.stream()
+ .mapToInt(WorkflowHistoryEvent::getEventId)
+ .filter(id -> id >= 0)
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("No rerunnable history event found"));
+ String rerunId = client.rerunWorkflowFromEvent(instanceId, rerunEventId,
+ new RerunWorkflowFromEventOptions().setInput("Osaka").setOverwriteInput(true));
+ System.out.printf("Reran workflow from event %d as new instance: %s%n", rerunEventId, rerunId);
+ client.waitForWorkflowCompletion(rerunId, null, true);
+
+ // List workflow instance IDs (first page).
+ WorkflowInstancePage page = client.listInstanceIds(null, 100);
+ System.out.printf("Listed %d instance ID(s); continuationToken=%s%n",
+ page.getInstanceIds().size(), page.getContinuationToken());
+ } catch (TimeoutException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorker.java b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorker.java
new file mode 100644
index 0000000000..24462f0110
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorker.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.examples.workflows.management;
+
+import io.dapr.examples.workflows.utils.PropertyUtils;
+import io.dapr.workflows.runtime.WorkflowRuntime;
+import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
+
+public class DemoWorkflowManagementWorker {
+ /**
+ * The main method of this app.
+ *
+ * @param args The port the app will listen on.
+ * @throws Exception An Exception.
+ */
+ public static void main(String[] args) throws Exception {
+ WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder(PropertyUtils.getProperties(args))
+ .registerWorkflow(DemoWorkflowManagementWorkflow.class);
+ builder.registerActivity(DemoWorkflowManagementActivity.class);
+
+ WorkflowRuntime runtime = builder.build();
+ System.out.println("Start workflow runtime");
+ runtime.start();
+ }
+}
diff --git a/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorkflow.java
new file mode 100644
index 0000000000..38e58b3196
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorkflow.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.examples.workflows.management;
+
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+
+public class DemoWorkflowManagementWorkflow implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow: " + ctx.getName());
+ String result = ctx.callActivity(
+ DemoWorkflowManagementActivity.class.getName(), "Tokyo", String.class).await();
+ ctx.getLogger().info("Workflow finished with result: " + result);
+ ctx.complete(result);
+ };
+ }
+}
diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java
index 96bd014415..b41e8982d8 100644
--- a/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java
+++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java
@@ -19,6 +19,8 @@
import io.dapr.durabletask.NewOrchestrationInstanceOptions;
import io.dapr.durabletask.OrchestrationMetadata;
import io.dapr.durabletask.PurgeResult;
+import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent;
+import io.dapr.durabletask.implementation.protobuf.OrchestratorService;
import io.dapr.utils.NetworkUtils;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.internal.ApiTokenClientInterceptor;
@@ -33,6 +35,7 @@
import javax.annotation.Nullable;
import java.time.Duration;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
@@ -570,19 +573,83 @@ public boolean purgeInstance(String workflowInstanceId) {
}
/**
- * Purges workflow instance state from the workflow state store.
+ * Lists workflow instance IDs. Returns the first page with no size limit.
*
- * @param workflowInstanceId The unique ID of the workflow instance to purge.
- * @return Return true if the workflow state was found and purged successfully otherwise false.
+ * @return a page of workflow instance IDs
*/
- public boolean purgeWorkflow(String workflowInstanceId) {
- PurgeResult result = this.innerClient.purgeInstance(workflowInstanceId);
+ public WorkflowInstancePage listInstanceIds() {
+ return this.listInstanceIds(null, null);
+ }
- if (result != null) {
- return result.getDeletedInstanceCount() > 0;
+ /**
+ * Lists workflow instance IDs with pagination.
+ *
+ * @param continuationToken the continuation token from a previous call, or null for the first page
+ * @param pageSize the maximum number of instance IDs to return, or null for no limit; must be
+ * greater than zero when set
+ * @return a page of workflow instance IDs and an optional continuation token for the next page
+ */
+ public WorkflowInstancePage listInstanceIds(@Nullable String continuationToken, @Nullable Integer pageSize) {
+ if (pageSize != null && pageSize <= 0) {
+ throw new IllegalArgumentException("pageSize must be greater than zero.");
+ }
+ OrchestratorService.ListInstanceIDsResponse response =
+ this.innerClient.listInstanceIds(continuationToken, pageSize);
+ return WorkflowClientConverter.toWorkflowInstancePage(response);
+ }
+
+ /**
+ * Gets the full execution history of a workflow instance.
+ *
+ * @param instanceId the unique ID of the workflow instance to get history for
+ * @return the list of history events for the workflow instance
+ */
+ public List getInstanceHistory(String instanceId) {
+ if (instanceId == null || instanceId.isEmpty()) {
+ throw new IllegalArgumentException("instanceId must not be null or empty.");
}
+ List events = this.innerClient.getInstanceHistory(instanceId);
+ return WorkflowClientConverter.toWorkflowHistory(events);
+ }
- return false;
+ /**
+ * Reruns a workflow from a history event, creating a new workflow instance.
+ *
+ * @param sourceInstanceId the ID of the source workflow instance to rerun from
+ * @param eventId the history event ID to rerun from; must not be negative
+ * @return the instance ID of the new workflow instance
+ * @throws IllegalArgumentException if sourceInstanceId is null or empty, or eventId is negative
+ */
+ public String rerunWorkflowFromEvent(String sourceInstanceId, int eventId) {
+ return this.rerunWorkflowFromEvent(sourceInstanceId, eventId, null);
+ }
+
+ /**
+ * Reruns a workflow from a history event with options, creating a new workflow instance.
+ *
+ * @param sourceInstanceId the ID of the source workflow instance to rerun from
+ * @param eventId the history event ID to rerun from; must not be negative
+ * @param options optional rerun configuration; may be null
+ * @return the instance ID of the new workflow instance
+ * @throws IllegalArgumentException if sourceInstanceId is null or empty, if eventId is negative,
+ * or if input is set on options without overwriteInput being true
+ */
+ public String rerunWorkflowFromEvent(String sourceInstanceId, int eventId,
+ @Nullable RerunWorkflowFromEventOptions options) {
+ if (sourceInstanceId == null || sourceInstanceId.isEmpty()) {
+ throw new IllegalArgumentException("sourceInstanceId must not be null or empty.");
+ }
+ if (eventId < 0) {
+ throw new IllegalArgumentException("eventId must not be negative.");
+ }
+ if (options == null) {
+ return this.innerClient.rerunWorkflowFromEvent(sourceInstanceId, eventId, null, null, false);
+ }
+ if (options.getInput() != null && !options.isOverwriteInput()) {
+ throw new IllegalArgumentException("overwriteInput must be true when input is set.");
+ }
+ return this.innerClient.rerunWorkflowFromEvent(sourceInstanceId, eventId,
+ options.getNewInstanceId(), options.getInput(), options.isOverwriteInput());
}
/**
@@ -608,6 +675,22 @@ public boolean purgeWorkflow(String workflowInstanceId, @Nullable String appId)
return false;
}
+ /**
+ * Purges workflow instance state from the workflow state store.
+ *
+ * @param workflowInstanceId The unique ID of the workflow instance to purge.
+ * @return Return true if the workflow state was found and purged successfully otherwise false.
+ */
+ public boolean purgeWorkflow(String workflowInstanceId) {
+ PurgeResult result = this.innerClient.purgeInstance(workflowInstanceId);
+
+ if (result != null) {
+ return result.getDeletedInstanceCount() > 0;
+ }
+
+ return false;
+ }
+
/**
* Closes the inner DurableTask client and shutdown the GRPC channel.
*/
diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/RerunWorkflowFromEventOptions.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/RerunWorkflowFromEventOptions.java
new file mode 100644
index 0000000000..4b20419471
--- /dev/null
+++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/RerunWorkflowFromEventOptions.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.workflows.client;
+
+import javax.annotation.Nullable;
+
+/**
+ * Options for the {@link DaprWorkflowClient#rerunWorkflowFromEvent(String, int, RerunWorkflowFromEventOptions)}
+ * operation.
+ */
+public final class RerunWorkflowFromEventOptions {
+
+ @Nullable
+ private String newInstanceId;
+ @Nullable
+ private Object input;
+ private boolean overwriteInput;
+
+ /**
+ * Sets the instance ID to use for the new workflow instance. When not set, a random ID is generated.
+ *
+ * @param newInstanceId the new instance ID
+ * @return this {@link RerunWorkflowFromEventOptions} object
+ */
+ public RerunWorkflowFromEventOptions setNewInstanceId(String newInstanceId) {
+ this.newInstanceId = newInstanceId;
+ return this;
+ }
+
+ /**
+ * Sets the input applied at the next activity event of the rerun instance. When set,
+ * {@link #setOverwriteInput(boolean)} must also be set to true.
+ *
+ * @param input the input to apply
+ * @return this {@link RerunWorkflowFromEventOptions} object
+ */
+ public RerunWorkflowFromEventOptions setInput(Object input) {
+ this.input = input;
+ return this;
+ }
+
+ /**
+ * Sets whether the input at the rerun point is overwritten with {@link #setInput(Object)}.
+ *
+ * @param overwriteInput true to overwrite the input
+ * @return this {@link RerunWorkflowFromEventOptions} object
+ */
+ public RerunWorkflowFromEventOptions setOverwriteInput(boolean overwriteInput) {
+ this.overwriteInput = overwriteInput;
+ return this;
+ }
+
+ /**
+ * Gets the new instance ID.
+ *
+ * @return the new instance ID, or null if not set
+ */
+ @Nullable
+ public String getNewInstanceId() {
+ return this.newInstanceId;
+ }
+
+ /**
+ * Gets the input to apply at the rerun point.
+ *
+ * @return the input, or null if not set
+ */
+ @Nullable
+ public Object getInput() {
+ return this.input;
+ }
+
+ /**
+ * Gets whether the input at the rerun point is overwritten.
+ *
+ * @return true if the input is overwritten
+ */
+ public boolean isOverwriteInput() {
+ return this.overwriteInput;
+ }
+}
diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowClientConverter.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowClientConverter.java
new file mode 100644
index 0000000000..11616ba8d6
--- /dev/null
+++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowClientConverter.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.workflows.client;
+
+import com.google.protobuf.Timestamp;
+import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent;
+import io.dapr.durabletask.implementation.protobuf.OrchestratorService.ListInstanceIDsResponse;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Converts durabletask proto messages to public workflow client model types.
+ */
+final class WorkflowClientConverter {
+
+ private WorkflowClientConverter() {
+ }
+
+ static WorkflowInstancePage toWorkflowInstancePage(ListInstanceIDsResponse response) {
+ return new WorkflowInstancePage(
+ new ArrayList<>(response.getInstanceIdsList()),
+ response.hasContinuationToken() ? response.getContinuationToken() : null);
+ }
+
+ static List toWorkflowHistory(List events) {
+ List result = new ArrayList<>(events.size());
+ for (HistoryEvent event : events) {
+ result.add(toWorkflowHistoryEvent(event));
+ }
+ return Collections.unmodifiableList(result);
+ }
+
+ static WorkflowHistoryEvent toWorkflowHistoryEvent(HistoryEvent event) {
+ Instant timestamp = event.hasTimestamp() ? toInstant(event.getTimestamp()) : Instant.EPOCH;
+ return new WorkflowHistoryEvent(event.getEventId(), toEventType(event.getEventTypeCase()), timestamp);
+ }
+
+ static WorkflowHistoryEventType toEventType(HistoryEvent.EventTypeCase eventType) {
+ switch (eventType) {
+ case EXECUTIONSTARTED:
+ return WorkflowHistoryEventType.EXECUTION_STARTED;
+ case EXECUTIONCOMPLETED:
+ return WorkflowHistoryEventType.EXECUTION_COMPLETED;
+ case EXECUTIONTERMINATED:
+ return WorkflowHistoryEventType.EXECUTION_TERMINATED;
+ case TASKSCHEDULED:
+ return WorkflowHistoryEventType.TASK_SCHEDULED;
+ case TASKCOMPLETED:
+ return WorkflowHistoryEventType.TASK_COMPLETED;
+ case TASKFAILED:
+ return WorkflowHistoryEventType.TASK_FAILED;
+ case CHILDWORKFLOWINSTANCECREATED:
+ return WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_CREATED;
+ case CHILDWORKFLOWINSTANCECOMPLETED:
+ return WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_COMPLETED;
+ case CHILDWORKFLOWINSTANCEFAILED:
+ return WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_FAILED;
+ case DETACHEDWORKFLOWINSTANCECREATED:
+ return WorkflowHistoryEventType.DETACHED_WORKFLOW_INSTANCE_CREATED;
+ case TIMERCREATED:
+ return WorkflowHistoryEventType.TIMER_CREATED;
+ case TIMERFIRED:
+ return WorkflowHistoryEventType.TIMER_FIRED;
+ case WORKFLOWSTARTED:
+ return WorkflowHistoryEventType.WORKFLOW_STARTED;
+ case WORKFLOWCOMPLETED:
+ return WorkflowHistoryEventType.WORKFLOW_COMPLETED;
+ case EVENTSENT:
+ return WorkflowHistoryEventType.EVENT_SENT;
+ case EVENTRAISED:
+ return WorkflowHistoryEventType.EVENT_RAISED;
+ case CONTINUEASNEW:
+ return WorkflowHistoryEventType.CONTINUE_AS_NEW;
+ case EXECUTIONSUSPENDED:
+ return WorkflowHistoryEventType.EXECUTION_SUSPENDED;
+ case EXECUTIONRESUMED:
+ return WorkflowHistoryEventType.EXECUTION_RESUMED;
+ case EXECUTIONSTALLED:
+ return WorkflowHistoryEventType.EXECUTION_STALLED;
+ default:
+ return WorkflowHistoryEventType.UNKNOWN;
+ }
+ }
+
+ private static Instant toInstant(Timestamp timestamp) {
+ return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos());
+ }
+}
diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEvent.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEvent.java
new file mode 100644
index 0000000000..9c01187159
--- /dev/null
+++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEvent.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.workflows.client;
+
+import java.time.Instant;
+
+/**
+ * Represents a single event in a workflow instance's execution history.
+ */
+public final class WorkflowHistoryEvent {
+
+ private final int eventId;
+ private final WorkflowHistoryEventType eventType;
+ private final Instant timestamp;
+
+ /**
+ * Constructs a workflow history event.
+ *
+ * @param eventId the event ID within the workflow instance history
+ * @param eventType the type of history event
+ * @param timestamp the time the event occurred
+ */
+ public WorkflowHistoryEvent(int eventId, WorkflowHistoryEventType eventType, Instant timestamp) {
+ this.eventId = eventId;
+ this.eventType = eventType;
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * Gets the event ID within the workflow instance history.
+ *
+ * @return the event ID
+ */
+ public int getEventId() {
+ return this.eventId;
+ }
+
+ /**
+ * Gets the type of this history event.
+ *
+ * @return the event type
+ */
+ public WorkflowHistoryEventType getEventType() {
+ return this.eventType;
+ }
+
+ /**
+ * Gets the time this event occurred.
+ *
+ * @return the event timestamp
+ */
+ public Instant getTimestamp() {
+ return this.timestamp;
+ }
+}
diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEventType.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEventType.java
new file mode 100644
index 0000000000..7775d42ccf
--- /dev/null
+++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEventType.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.workflows.client;
+
+/**
+ * Represents the type of a workflow history event.
+ */
+public enum WorkflowHistoryEventType {
+ /**
+ * Unknown or unmapped event type.
+ */
+ UNKNOWN,
+
+ /**
+ * The workflow execution started.
+ */
+ EXECUTION_STARTED,
+
+ /**
+ * The workflow execution completed.
+ */
+ EXECUTION_COMPLETED,
+
+ /**
+ * The workflow execution was terminated.
+ */
+ EXECUTION_TERMINATED,
+
+ /**
+ * An activity task was scheduled.
+ */
+ TASK_SCHEDULED,
+
+ /**
+ * An activity task completed successfully.
+ */
+ TASK_COMPLETED,
+
+ /**
+ * An activity task failed.
+ */
+ TASK_FAILED,
+
+ /**
+ * A child workflow instance was created.
+ */
+ CHILD_WORKFLOW_INSTANCE_CREATED,
+
+ /**
+ * A child workflow instance completed.
+ */
+ CHILD_WORKFLOW_INSTANCE_COMPLETED,
+
+ /**
+ * A child workflow instance failed.
+ */
+ CHILD_WORKFLOW_INSTANCE_FAILED,
+
+ /**
+ * A detached workflow instance was created. Unlike a child workflow, a detached instance has no
+ * parent linkage, so no completion or failure event flows back to the workflow that created it.
+ */
+ DETACHED_WORKFLOW_INSTANCE_CREATED,
+
+ /**
+ * A timer was created.
+ */
+ TIMER_CREATED,
+
+ /**
+ * A timer fired.
+ */
+ TIMER_FIRED,
+
+ /**
+ * The workflow started processing a work item.
+ */
+ WORKFLOW_STARTED,
+
+ /**
+ * The workflow completed processing a work item.
+ */
+ WORKFLOW_COMPLETED,
+
+ /**
+ * An event was sent to another instance.
+ */
+ EVENT_SENT,
+
+ /**
+ * An external event was raised.
+ */
+ EVENT_RAISED,
+
+ /**
+ * The workflow continued as new.
+ */
+ CONTINUE_AS_NEW,
+
+ /**
+ * The workflow execution was suspended.
+ */
+ EXECUTION_SUSPENDED,
+
+ /**
+ * The workflow execution was resumed.
+ */
+ EXECUTION_RESUMED,
+
+ /**
+ * The workflow execution stalled.
+ */
+ EXECUTION_STALLED
+}
diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstancePage.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstancePage.java
new file mode 100644
index 0000000000..4f05797d3e
--- /dev/null
+++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstancePage.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.workflows.client;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a page of workflow instance IDs returned by a list operation.
+ */
+public final class WorkflowInstancePage {
+
+ private final List instanceIds;
+ @Nullable
+ private final String continuationToken;
+
+ /**
+ * Constructs a page of workflow instance IDs.
+ *
+ * @param instanceIds the workflow instance IDs in this page; must not be null
+ * @param continuationToken the token used to retrieve the next page, or null if there are no more pages
+ */
+ public WorkflowInstancePage(List instanceIds, @Nullable String continuationToken) {
+ this.instanceIds = Collections.unmodifiableList(new ArrayList<>(instanceIds));
+ this.continuationToken = continuationToken;
+ }
+
+ /**
+ * Gets the workflow instance IDs in this page.
+ *
+ * @return an unmodifiable list of instance IDs
+ */
+ public List getInstanceIds() {
+ return this.instanceIds;
+ }
+
+ /**
+ * Gets the continuation token for the next page.
+ *
+ * @return the continuation token, or null if there are no more pages
+ */
+ @Nullable
+ public String getContinuationToken() {
+ return this.continuationToken;
+ }
+}
diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java
index edf2138f9c..950e05f3a5 100644
--- a/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java
+++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java
@@ -13,12 +13,16 @@
package io.dapr.workflows.client;
+import com.google.protobuf.Timestamp;
import io.dapr.config.Properties;
import io.dapr.durabletask.DurableTaskClient;
import io.dapr.durabletask.DurableTaskGrpcClientBuilder;
import io.dapr.durabletask.NewOrchestrationInstanceOptions;
import io.dapr.durabletask.OrchestrationMetadata;
import io.dapr.durabletask.OrchestrationRuntimeStatus;
+import io.dapr.durabletask.implementation.protobuf.HistoryEvents;
+import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent;
+import io.dapr.durabletask.implementation.protobuf.OrchestratorService.ListInstanceIDsResponse;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowContext;
import io.dapr.workflows.WorkflowStub;
@@ -36,16 +40,20 @@
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeoutException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
@@ -390,6 +398,104 @@ public void purgeInstance() {
verify(mockInnerClient, times(1)).purgeInstance(expectedArgument);
}
+ @Test
+ public void listInstanceIds() {
+ ListInstanceIDsResponse response = ListInstanceIDsResponse.newBuilder()
+ .addInstanceIds("id-1").addInstanceIds("id-2")
+ .setContinuationToken("next-token")
+ .build();
+ when(mockInnerClient.listInstanceIds("tok", 50)).thenReturn(response);
+
+ WorkflowInstancePage page = client.listInstanceIds("tok", 50);
+
+ verify(mockInnerClient, times(1)).listInstanceIds("tok", 50);
+ assertEquals(Arrays.asList("id-1", "id-2"), page.getInstanceIds());
+ assertEquals("next-token", page.getContinuationToken());
+ }
+
+ @Test
+ public void listInstanceIdsNoArgs() {
+ ListInstanceIDsResponse response = ListInstanceIDsResponse.newBuilder().addInstanceIds("id-1").build();
+ when(mockInnerClient.listInstanceIds(null, null)).thenReturn(response);
+
+ WorkflowInstancePage page = client.listInstanceIds();
+
+ verify(mockInnerClient, times(1)).listInstanceIds(null, null);
+ assertEquals(Arrays.asList("id-1"), page.getInstanceIds());
+ assertNull(page.getContinuationToken());
+ }
+
+ @Test
+ public void listInstanceIdsRejectsNonPositivePageSize() {
+ assertThrows(IllegalArgumentException.class, () -> client.listInstanceIds(null, 0));
+ verify(mockInnerClient, never()).listInstanceIds(any(), any());
+ }
+
+ @Test
+ public void getInstanceHistory() {
+ HistoryEvent event = HistoryEvent.newBuilder()
+ .setEventId(1)
+ .setTimestamp(Timestamp.newBuilder().setSeconds(10).build())
+ .setExecutionStarted(HistoryEvents.ExecutionStartedEvent.getDefaultInstance())
+ .build();
+ when(mockInnerClient.getInstanceHistory("wf-1")).thenReturn(Arrays.asList(event));
+
+ List history = client.getInstanceHistory("wf-1");
+
+ verify(mockInnerClient, times(1)).getInstanceHistory("wf-1");
+ assertEquals(1, history.size());
+ assertEquals(1, history.get(0).getEventId());
+ assertEquals(WorkflowHistoryEventType.EXECUTION_STARTED, history.get(0).getEventType());
+ }
+
+ @Test
+ public void getInstanceHistoryRejectsEmptyId() {
+ assertThrows(IllegalArgumentException.class, () -> client.getInstanceHistory(""));
+ verify(mockInnerClient, never()).getInstanceHistory(any());
+ }
+
+ @Test
+ public void rerunWorkflowFromEvent() {
+ when(mockInnerClient.rerunWorkflowFromEvent("src", 2, null, null, false)).thenReturn("new-id");
+
+ String newId = client.rerunWorkflowFromEvent("src", 2);
+
+ verify(mockInnerClient, times(1)).rerunWorkflowFromEvent("src", 2, null, null, false);
+ assertEquals("new-id", newId);
+ }
+
+ @Test
+ public void rerunWorkflowFromEventWithOptions() {
+ RerunWorkflowFromEventOptions options = new RerunWorkflowFromEventOptions()
+ .setNewInstanceId("target").setInput("payload").setOverwriteInput(true);
+ when(mockInnerClient.rerunWorkflowFromEvent("src", 3, "target", "payload", true)).thenReturn("target");
+
+ String newId = client.rerunWorkflowFromEvent("src", 3, options);
+
+ verify(mockInnerClient, times(1)).rerunWorkflowFromEvent("src", 3, "target", "payload", true);
+ assertEquals("target", newId);
+ }
+
+ @Test
+ public void rerunWorkflowFromEventRejectsInputWithoutOverwrite() {
+ RerunWorkflowFromEventOptions options = new RerunWorkflowFromEventOptions().setInput("payload");
+ assertThrows(IllegalArgumentException.class, () -> client.rerunWorkflowFromEvent("src", 1, options));
+ verify(mockInnerClient, never()).rerunWorkflowFromEvent(any(), anyInt(), any(), any(), anyBoolean());
+ }
+
+ @Test
+ public void rerunWorkflowFromEventRejectsNegativeEventId() {
+ assertThrows(IllegalArgumentException.class, () -> client.rerunWorkflowFromEvent("src", -1));
+ verify(mockInnerClient, never()).rerunWorkflowFromEvent(any(), anyInt(), any(), any(), anyBoolean());
+ }
+
+ @Test
+ public void rerunWorkflowFromEventWithOptionsRejectsNegativeEventId() {
+ RerunWorkflowFromEventOptions options = new RerunWorkflowFromEventOptions().setNewInstanceId("target");
+ assertThrows(IllegalArgumentException.class, () -> client.rerunWorkflowFromEvent("src", -1, options));
+ verify(mockInnerClient, never()).rerunWorkflowFromEvent(any(), anyInt(), any(), any(), anyBoolean());
+ }
+
@Test
public void scheduleNewWorkflowWithAppIdOption() {
String expectedName = TestWorkflow.class.getCanonicalName();
diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/RerunWorkflowFromEventOptionsTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/RerunWorkflowFromEventOptionsTest.java
new file mode 100644
index 0000000000..2c443e5b95
--- /dev/null
+++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/RerunWorkflowFromEventOptionsTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.workflows.client;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+public class RerunWorkflowFromEventOptionsTest {
+
+ @Test
+ public void fluentSettersReturnSameInstanceAndStoreValues() {
+ RerunWorkflowFromEventOptions options = new RerunWorkflowFromEventOptions();
+ assertSame(options, options.setNewInstanceId("target"));
+ assertSame(options, options.setInput("payload"));
+ assertSame(options, options.setOverwriteInput(true));
+
+ assertEquals("target", options.getNewInstanceId());
+ assertEquals("payload", options.getInput());
+ assertEquals(true, options.isOverwriteInput());
+ }
+
+ @Test
+ public void overwriteInputDefaultsToFalse() {
+ assertFalse(new RerunWorkflowFromEventOptions().isOverwriteInput());
+ }
+}
diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowClientConverterTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowClientConverterTest.java
new file mode 100644
index 0000000000..84391f0cd3
--- /dev/null
+++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowClientConverterTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.workflows.client;
+
+import com.google.protobuf.Timestamp;
+import io.dapr.durabletask.implementation.protobuf.HistoryEvents;
+import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent;
+import io.dapr.durabletask.implementation.protobuf.OrchestratorService.ListInstanceIDsResponse;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class WorkflowClientConverterTest {
+
+ @Test
+ public void mapsEventTypeCases() {
+ // Execution events
+ assertEquals(WorkflowHistoryEventType.EXECUTION_STARTED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONSTARTED));
+ assertEquals(WorkflowHistoryEventType.EXECUTION_COMPLETED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONCOMPLETED));
+ assertEquals(WorkflowHistoryEventType.EXECUTION_TERMINATED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONTERMINATED));
+ assertEquals(WorkflowHistoryEventType.EXECUTION_SUSPENDED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONSUSPENDED));
+ assertEquals(WorkflowHistoryEventType.EXECUTION_RESUMED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONRESUMED));
+ assertEquals(WorkflowHistoryEventType.EXECUTION_STALLED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONSTALLED));
+
+ // Task events
+ assertEquals(WorkflowHistoryEventType.TASK_SCHEDULED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TASKSCHEDULED));
+ assertEquals(WorkflowHistoryEventType.TASK_COMPLETED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TASKCOMPLETED));
+ assertEquals(WorkflowHistoryEventType.TASK_FAILED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TASKFAILED));
+
+ // Child workflow events
+ assertEquals(WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_CREATED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.CHILDWORKFLOWINSTANCECREATED));
+ assertEquals(WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_COMPLETED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.CHILDWORKFLOWINSTANCECOMPLETED));
+ assertEquals(WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_FAILED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.CHILDWORKFLOWINSTANCEFAILED));
+
+ // Detached workflow events
+ assertEquals(WorkflowHistoryEventType.DETACHED_WORKFLOW_INSTANCE_CREATED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.DETACHEDWORKFLOWINSTANCECREATED));
+
+ // Timer events
+ assertEquals(WorkflowHistoryEventType.TIMER_CREATED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TIMERCREATED));
+ assertEquals(WorkflowHistoryEventType.TIMER_FIRED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TIMERFIRED));
+
+ // Workflow events
+ assertEquals(WorkflowHistoryEventType.WORKFLOW_STARTED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.WORKFLOWSTARTED));
+ assertEquals(WorkflowHistoryEventType.WORKFLOW_COMPLETED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.WORKFLOWCOMPLETED));
+
+ // Event communication
+ assertEquals(WorkflowHistoryEventType.EVENT_SENT,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EVENTSENT));
+ assertEquals(WorkflowHistoryEventType.EVENT_RAISED,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EVENTRAISED));
+
+ // Continue as new
+ assertEquals(WorkflowHistoryEventType.CONTINUE_AS_NEW,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.CONTINUEASNEW));
+
+ // Unknown/unset
+ assertEquals(WorkflowHistoryEventType.UNKNOWN,
+ WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EVENTTYPE_NOT_SET));
+ }
+
+ @Test
+ public void mapsHistoryEvent() {
+ HistoryEvent event = HistoryEvent.newBuilder()
+ .setEventId(7)
+ .setTimestamp(Timestamp.newBuilder().setSeconds(1500).setNanos(500).build())
+ .setExecutionStarted(HistoryEvents.ExecutionStartedEvent.getDefaultInstance())
+ .build();
+
+ WorkflowHistoryEvent result = WorkflowClientConverter.toWorkflowHistoryEvent(event);
+
+ assertEquals(7, result.getEventId());
+ assertEquals(WorkflowHistoryEventType.EXECUTION_STARTED, result.getEventType());
+ assertEquals(Instant.ofEpochSecond(1500, 500), result.getTimestamp());
+ }
+
+ @Test
+ public void mapsHistoryList() {
+ HistoryEvent event = HistoryEvent.newBuilder()
+ .setEventId(1)
+ .setTimerCreated(HistoryEvents.TimerCreatedEvent.getDefaultInstance())
+ .build();
+
+ assertEquals(1, WorkflowClientConverter.toWorkflowHistory(Arrays.asList(event)).size());
+ assertEquals(WorkflowHistoryEventType.TIMER_CREATED,
+ WorkflowClientConverter.toWorkflowHistory(Arrays.asList(event)).get(0).getEventType());
+ }
+
+ @Test
+ public void usesEpochTimestampWhenNotSet() {
+ HistoryEvent event = HistoryEvent.newBuilder()
+ .setEventId(42)
+ .setTaskScheduled(HistoryEvents.TaskScheduledEvent.getDefaultInstance())
+ .build();
+
+ WorkflowHistoryEvent result = WorkflowClientConverter.toWorkflowHistoryEvent(event);
+
+ assertEquals(Instant.EPOCH, result.getTimestamp());
+ }
+
+ @Test
+ public void mapsInstancePageWithToken() {
+ ListInstanceIDsResponse response = ListInstanceIDsResponse.newBuilder()
+ .addInstanceIds("a").addInstanceIds("b")
+ .setContinuationToken("next")
+ .build();
+
+ WorkflowInstancePage page = WorkflowClientConverter.toWorkflowInstancePage(response);
+
+ assertEquals(Arrays.asList("a", "b"), page.getInstanceIds());
+ assertEquals("next", page.getContinuationToken());
+ }
+
+ @Test
+ public void mapsInstancePageWithoutToken() {
+ ListInstanceIDsResponse response = ListInstanceIDsResponse.newBuilder().addInstanceIds("a").build();
+
+ WorkflowInstancePage page = WorkflowClientConverter.toWorkflowInstancePage(response);
+
+ assertNull(page.getContinuationToken());
+ }
+}
diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowHistoryEventTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowHistoryEventTest.java
new file mode 100644
index 0000000000..e071167f8b
--- /dev/null
+++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowHistoryEventTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.workflows.client;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class WorkflowHistoryEventTest {
+
+ @Test
+ public void exposesFields() {
+ Instant now = Instant.ofEpochSecond(1000, 5);
+ WorkflowHistoryEvent event = new WorkflowHistoryEvent(3, WorkflowHistoryEventType.TASK_SCHEDULED, now);
+ assertEquals(3, event.getEventId());
+ assertEquals(WorkflowHistoryEventType.TASK_SCHEDULED, event.getEventType());
+ assertEquals(now, event.getTimestamp());
+ }
+}
diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstancePageTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstancePageTest.java
new file mode 100644
index 0000000000..2395c332cd
--- /dev/null
+++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstancePageTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.workflows.client;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class WorkflowInstancePageTest {
+
+ @Test
+ public void exposesInstanceIdsAndToken() {
+ WorkflowInstancePage page = new WorkflowInstancePage(Arrays.asList("a", "b"), "next");
+ assertEquals(Arrays.asList("a", "b"), page.getInstanceIds());
+ assertEquals("next", page.getContinuationToken());
+ }
+
+ @Test
+ public void allowsNullContinuationToken() {
+ WorkflowInstancePage page = new WorkflowInstancePage(Arrays.asList("a"), null);
+ assertNull(page.getContinuationToken());
+ }
+
+ @Test
+ public void instanceIdsListIsUnmodifiable() {
+ WorkflowInstancePage page = new WorkflowInstancePage(Arrays.asList("a"), null);
+ assertThrows(UnsupportedOperationException.class, () -> page.getInstanceIds().add("b"));
+ }
+}