Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2b4bb17
docs: design spec for workflow list/history/rerun (#1794)
siri-varma Aug 26, 2026
b5bf3e9
docs: implementation plan for workflow list/history/rerun (#1794)
siri-varma Aug 26, 2026
019cc04
feat(workflows): add list/history/rerun model types (#1794)
siri-varma Aug 26, 2026
d566e78
feat(workflows): add proto-to-model converter for history/list (#1794)
siri-varma Aug 26, 2026
bc490e0
test(workflows): cover all EventTypeCase mappings and EPOCH fallback …
siri-varma Aug 26, 2026
89d1285
feat(durabletask): add list/history/rerun gRPC operations (#1794)
siri-varma Aug 26, 2026
5f0ee6a
feat(workflows): expose list/history/rerun on DaprWorkflowClient (#1794)
siri-varma Aug 26, 2026
61acefd
test(durabletask): integration tests for list/history/rerun (#1794)
siri-varma Aug 27, 2026
d8308a5
docs(examples): add workflow list/history/rerun example (#1794)
siri-varma Aug 27, 2026
cfdfff6
fix(durabletask): guard null serialized input in rerunWorkflowFromEve…
siri-varma Aug 27, 2026
3b4d410
Remove specs
siri-varma Aug 28, 2026
ed014af
fix(examples): share worker sidecar in workflow management example (#…
siri-varma Sep 6, 2026
ac9b8f6
test(durabletask): remove list/history/rerun ITs that exceed CI budge…
siri-varma Sep 8, 2026
08053c2
fix(examples): rerun from a valid history event in management example…
siri-varma Sep 8, 2026
2671e1f
fix(examples): use substring matching for workflow management example…
siri-varma Sep 9, 2026
f304e91
fix(workflows): address review feedback on list/history/rerun (#1794)
siri-varma Sep 14, 2026
821b10a
Merge branch 'master' into feature/1794-workflow-list-history-rerun
siri-varma Sep 15, 2026
ef5f77a
Add method to resume orchestration from another app
siri-varma Sep 15, 2026
dfa4fcd
Rearrange
siri-varma Sep 15, 2026
5526e93
Rearrange
siri-varma Sep 15, 2026
113812b
Rearrange
siri-varma Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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.
*
* <p>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.</p>
*
* @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.
*
Expand Down Expand Up @@ -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<HistoryEvent> getInstanceHistory(String instanceId);

/**
* Resumes a running orchestration instance owned by another app.
*
* <p>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.</p>
*
* @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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<HistoryEvent> 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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1745,7 +1745,6 @@ public void taskExecutionIdTest() {
}

}

}


53 changes: 52 additions & 1 deletion examples/src/main/java/io/dapr/examples/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```
```

### 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.

<!-- STEP
name: Run Workflow Management Worker
match_order: none
output_match_mode: substring
expected_stdout_lines:
- "Start workflow runtime"
background: true
sleep: 20
timeout_seconds: 45
-->

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
```

<!-- END_STEP -->

<!-- STEP
name: Run Workflow Management Client
match_order: none
output_match_mode: substring
expected_stdout_lines:
- "Started a new workflow with instance ID"
- "Workflow completed with result"
- "Reran workflow from event"
- "Listed"
timeout_seconds: 60
-->

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
```

<!-- END_STEP -->

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.
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<WorkflowHistoryEvent> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading
Loading