Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ to docs, or any other relevant information.
alters the size of payloads (e.g. compression, encryption, external storage), it is advised that
you disable size enforcement by setting `disablePayloadErrorLimit: true` on the worker.

### Fixed

- `WorkflowExecutionAlreadyStartedError` now exposes the `runId` of the already-running Workflow Execution when the server provides it in the error details (#1838).

## [1.20.3] - 2026-07-13

### Fixed
Expand Down
21 changes: 21 additions & 0 deletions packages/client/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,27 @@ export function extractNexusOperationAlreadyStartedRunId(err: GrpcServiceError):
return undefined;
}

/**
* Try to extract the runId field from a gRPC ALREADY_EXISTS error's details containing
* WorkflowExecutionAlreadyStartedFailure. Returns undefined if the error details
* do not include this failure type or cannot be decoded.
*/
export function extractWorkflowExecutionAlreadyStartedRunId(err: GrpcServiceError): string | undefined {
try {
for (const entry of getGrpcStatusDetails(err) ?? []) {
if (!entry.type_url || !entry.value) continue;
const type = entry.type_url.replace(/^type.googleapis.com\//, '');
if (type !== 'temporal.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure') continue;

const details = temporal.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure.decode(entry.value);
return details.runId || undefined;
}
} catch {
// ignore
}
return undefined;
}

export function trimGrpcTypeUrl(type_url: string | null | undefined): string {
return type_url?.replace(/^type.googleapis.com\//, '') || '';
}
16 changes: 12 additions & 4 deletions packages/client/src/workflow-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ import type {
WorkflowUpdateOptions,
} from './workflow-options';
import { compileWorkflowOptions } from './workflow-options';
import { decodeCountWorkflowExecutionsResponse, executionInfoFromRaw, rethrowKnownErrorTypes } from './helpers';
import {
decodeCountWorkflowExecutionsResponse,
executionInfoFromRaw,
extractWorkflowExecutionAlreadyStartedRunId,
rethrowKnownErrorTypes,
} from './helpers';
import type { BaseClientOptions, LoadedWithDefaults, WithDefaults } from './base-client';
import { BaseClient, defaultBaseClientOptions } from './base-client';
import { mapAsyncIterable } from './iterators-utils';
Expand Down Expand Up @@ -1134,7 +1139,8 @@ export class WorkflowClient extends BaseClient {
err = new WorkflowExecutionAlreadyStartedError(
'Workflow execution already started',
input.workflowStartOptions.workflowId,
input.workflowType
input.workflowType,
extractWorkflowExecutionAlreadyStartedRunId(err)
);
}
if (!seenStart) {
Expand Down Expand Up @@ -1293,7 +1299,8 @@ export class WorkflowClient extends BaseClient {
throw new WorkflowExecutionAlreadyStartedError(
'Workflow execution already started',
options.workflowId,
workflowType
workflowType,
extractWorkflowExecutionAlreadyStartedRunId(err)
);
}
this.rethrowGrpcError(err, 'Failed to signalWithStart Workflow', { workflowId: options.workflowId });
Expand Down Expand Up @@ -1323,7 +1330,8 @@ export class WorkflowClient extends BaseClient {
throw new WorkflowExecutionAlreadyStartedError(
'Workflow execution already started',
opts.workflowId,
workflowType
workflowType,
extractWorkflowExecutionAlreadyStartedRunId(err)
);
}
this.rethrowGrpcError(err, 'Failed to start Workflow', { workflowId: opts.workflowId });
Expand Down
6 changes: 5 additions & 1 deletion packages/common/src/failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,11 @@ export class WorkflowExecutionAlreadyStartedError extends TemporalFailure {
constructor(
message: string,
public readonly workflowId: string,
public readonly workflowType: string
public readonly workflowType: string,
/**
* The runId of the already-running Workflow Execution, when provided by the server.
*/
public readonly runId?: string
) {
super(message);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/test/src/test-integration-workflow-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ test('Start of workflow respects workflow id conflict policy', async (t) => {
);

t.true(err instanceof WorkflowExecutionAlreadyStartedError);
t.is((err as WorkflowExecutionAlreadyStartedError).runId, handle.firstExecutionRunId);

// Confirm fails with explicit option
const err1 = await t.throwsAsync(
Expand All @@ -86,6 +87,7 @@ test('Start of workflow respects workflow id conflict policy', async (t) => {
);

t.true(err1 instanceof WorkflowExecutionAlreadyStartedError);
t.is((err1 as WorkflowExecutionAlreadyStartedError).runId, handle.firstExecutionRunId);

// Confirm gives back same handle
const handle2 = await client.workflow.start(conflictId, {
Expand Down