Durable, resumable export of terminal orchestration history to Azure Blob Storage for the Durable Task Java SDK — for compliance, audit, and offline analysis before instances age out of the task hub.
This module uses the same checkpointed entity-and-orchestrator workflow as the .NET Microsoft.DurableTask.ExportHistory (preview) feature: it pages terminal instances by completion window, fans out per-instance export activities, and uploads serialized history (gzipped JSONL by default) to a customer-owned blob container.
Status: preview (
1.10.0-preview.1).
Add the module dependency alongside the core client (and your Durable Task Scheduler extension):
implementation 'com.microsoft:durabletask-exporthistory:1.10.0-preview.1'The export activities upload to Azure Blob Storage via azure-storage-blob. If you authenticate with a managed
identity, also add com.azure:azure-identity to your application.
// Storage destination for exported history (Azure Blob)
ExportHistoryStorageOptions storage = new ExportHistoryStorageOptions()
.setConnectionString(System.getenv("EXPORT_HISTORY_STORAGE_CONNECTION_STRING"))
.setContainerName("orchestration-history")
.setPrefix("exports/"); // optional
// identity alt: .setAccountUri(uri).setCredential(new DefaultAzureCredentialBuilder().build())
// Build a client first — the export activities need a client to the same backend.
DurableTaskGrpcClientBuilder clientBuilder = new DurableTaskGrpcClientBuilder();
DurableTaskSchedulerClientExtensions.useDurableTaskScheduler(clientBuilder, dtsConn);
DurableTaskClient client = clientBuilder.build();
// Worker: register the export entity + orchestrators + activities (uploads run here).
DurableTaskGrpcWorkerBuilder workerBuilder = new DurableTaskGrpcWorkerBuilder();
DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler(workerBuilder, dtsConn);
ExportHistoryWorkerExtensions.useExportHistory(workerBuilder, storage, client);
// Client: obtain an ExportHistoryClient bound to the destination.
ExportHistoryClient export = ExportHistoryClientExtensions.useExportHistory(client, storage);
// Create a job: archive everything completed in a window.
ExportHistoryJobClient job = export.createJob(new ExportJobCreationOptions("nightly-archive")
.setMode(ExportMode.BATCH)
.setCompletedTimeFrom(Instant.parse("2026-06-01T00:00:00Z"))
.setCompletedTimeTo(Instant.parse("2026-06-25T00:00:00Z"))
.setRuntimeStatus(List.of(OrchestrationRuntimeStatus.COMPLETED))
.setMaxInstancesPerBatch(200)); // 1–1000, default 100
// Inspect progress.
ExportJobDescription d = job.describe();
System.out.println(d.getStatus() + " exported=" + d.getExportedInstances());- BATCH — exports a fixed completion-time window and completes. Requires
completedTimeFromandcompletedTimeTo(the upper bound must not be in the future). - CONTINUOUS — tails newly-completed terminal instances on a 1-minute idle loop until the job is deleted.
When
completedTimeFromis omitted, the lower bound is captured whenExportJobCreationOptionsis constructed, matching .NET. It is preserved through job submission, so worker queue delays do not move the window forward.
Export supports terminal orchestration statuses only: COMPLETED, FAILED, TERMINATED. When no status filter is
supplied, all three are exported.
- DTS connection — the one your app already uses; no new value.
- Blob destination — a container name plus either a storage connection string or identity
(
AccountUri+TokenCredential). Prefix and format (JSONL + gzip) are optional with defaults. The storage secret is held worker-side, not persisted in task-hub state. - Permissions — the storage credential needs blob write on the container; the DTS credential needs orchestration read.
The worker ensures the export container exists before every upload, recreating it if it was deleted between exports. The storage credential therefore also needs permission to create the container.
Each blob holds the instance's full history using .NET-style history-event JSON. Golden fixtures cover core event serialization and the legacy entity-message encoding produced by the .NET SDK and Microsoft.Azure.DurableTask.Core:
- JSONL (default, gzipped) is one JSON object per line; JSON is a single array.
- Each event is
{"eventType": "...", <type-specific fields>, "eventId": N, "isPlayed": false, "timestamp": "..."}. - camelCase field names, null event fields omitted, empty maps as
{}, enum values in PascalCase (e.g."Completed"), timestamps as trimmed ISO-8601 ending inZ, and HTML-safe string escaping ("→\u0022,& < > ' +and all non-ASCII →\uXXXX). - Sub-millisecond timestamps are preserved when reading history and metadata. Exported timestamps and blob-name timestamps use .NET's 100-nanosecond precision; orchestration replay timestamp behavior is unchanged.
- Ordinary and nested failure details retain custom
properties, including nested values and date tags projected to .NET-style timestamps. Failure details read from streamed history retain empty property objects as{}. - Entity operations and locks are exported as
EventSent/EventRaisedevents. Theirinputcontains the reference SDK's JSON-encoded entity request or response, including parent orchestration context and nested failure details. This replaces the earlier Java-nativeEntity...event format; consumers of that preview format must update their event handling. The public history API continues returning typed Java entity events. - JSONL uses LF line endings. Identical compressed bytes or identical fields across all SDK versions are not guaranteed.
Blob names: a lowercase-hex SHA-256 of "<completedTimestamp>|<instanceId>" plus the format extension. Re-exporting an instance previously named using millisecond-truncated metadata can produce a new blob name; existing export blobs are not renamed or removed.
The export feature relies on the ListInstanceIds and StreamInstanceHistory gRPC operations. Managed DTS serves
both; the emulator / self-hosted sidecar needs ≥ v0.4.22. Against an older backend, a raw gRPC UNIMPLEMENTED
surfaces (matching .NET).
Locally, with the DTS emulator and Azurite:
Run the automated suite with ./gradlew :exporthistory:integrationTest -PskipSigning after starting the backends.
It checks batch export, JSON and gzipped JSONL failure properties, resolved large-payload history, container
recreation after deletion, and continuous export of an orchestration completed between options construction
and job creation.
- Start the backends:
docker run --name durabletask-emulator -p 4001:8080 -d mcr.microsoft.com/dts/dts-emulator:latest docker run --name azurite -p 10000:10000 -d mcr.microsoft.com/azure-storage/azurite azurite-blob --blobHost 0.0.0.0 - Point the app at them: DTS connection
Endpoint=http://localhost:4001;Authentication=None, storage = the Azurite dev connection string, containerorchestration-history. - Run an orchestration to a terminal state, then create a
BATCHexport job whose window covers its completion time. - Confirm the job reaches
COMPLETEDand inspect progress:ExportJobDescription d = job.describe(); // d.getStatus() == ExportJobStatus.COMPLETED, d.getExportedInstances() >= 1
- Download the blob from the container (gunzip for JSONL) and inspect it. Every line carries an
eventTypediscriminator,isPlayed:false, and a trailingtimestamp.
See HistoryExportSample:
./gradlew :samples:runHistoryExportSample