diff --git a/packages/client/src/schedule-helpers.ts b/packages/client/src/schedule-helpers.ts index e403b7cce..34d3179c3 100644 --- a/packages/client/src/schedule-helpers.ts +++ b/packages/client/src/schedule-helpers.ts @@ -1,5 +1,11 @@ import Long from 'long'; // eslint-disable-line import/no-named-as-default -import { compileRetryPolicy, decompileRetryPolicy, extractWorkflowType, LoadedDataConverter } from '@temporalio/common'; +import { + compileRetryPolicy, + decompileRetryPolicy, + extractWorkflowType, + JsonPayloadConverter, + LoadedDataConverter, +} from '@temporalio/common'; import { encodeUnifiedSearchAttributes, decodeSearchAttributes, @@ -189,8 +195,7 @@ export function decodeOptionalStructuredCalendarSpecs( } export function compileScheduleOptions(options: ScheduleOptions): CompiledScheduleOptions { - const workflowTypeOrFunc = options.action.workflowType; - const workflowType = extractWorkflowType(workflowTypeOrFunc); + const workflowType = extractWorkflowType(options.action.workflowType); return { ...options, action: { @@ -240,6 +245,7 @@ export async function encodeScheduleAction( action: CompiledScheduleAction, headers: Headers ): Promise { + const jsonConverter = new JsonPayloadConverter(); return { startWorkflow: { workflowId: action.workflowId, @@ -263,6 +269,10 @@ export async function encodeScheduleAction( } : undefined, header: { fields: headers }, + userMetadata: { + summary: jsonConverter.toPayload(action.staticSummary), + details: jsonConverter.toPayload(action.staticDetails), + }, }, }; } @@ -312,6 +322,8 @@ export async function decodeScheduleAction( pb: temporal.api.schedule.v1.IScheduleAction ): Promise { if (pb.startWorkflow) { + const jsonConverter = new JsonPayloadConverter(); + const userMetadata = pb.startWorkflow?.userMetadata; return { type: 'startWorkflow', // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -328,6 +340,8 @@ export async function decodeScheduleAction( workflowExecutionTimeout: optionalTsToMs(pb.startWorkflow.workflowExecutionTimeout), workflowRunTimeout: optionalTsToMs(pb.startWorkflow.workflowRunTimeout), workflowTaskTimeout: optionalTsToMs(pb.startWorkflow.workflowTaskTimeout), + staticSummary: userMetadata?.summary ? jsonConverter.fromPayload(userMetadata.summary) : undefined, + staticDetails: userMetadata?.details ? jsonConverter.fromPayload(userMetadata.details) : undefined, }; } throw new TypeError('Unsupported schedule action'); diff --git a/packages/client/src/schedule-types.ts b/packages/client/src/schedule-types.ts index 176e62e0a..7f57ab9ae 100644 --- a/packages/client/src/schedule-types.ts +++ b/packages/client/src/schedule-types.ts @@ -783,6 +783,8 @@ export type ScheduleOptionsStartWorkflowAction = { | 'workflowExecutionTimeout' | 'workflowRunTimeout' | 'workflowTaskTimeout' + | 'staticDetails' + | 'staticSummary' > & { /** * Workflow id to use when starting. Assign a meaningful business id. @@ -815,6 +817,8 @@ export type ScheduleDescriptionStartWorkflowAction = ScheduleSummaryStartWorkflo | 'workflowExecutionTimeout' | 'workflowRunTimeout' | 'workflowTaskTimeout' + | 'staticSummary' + | 'staticDetails' >; // Invariant: an existing ScheduleDescriptionAction can be used as is to create or update a schedule diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 7289f0e05..f4c0c8c19 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -65,6 +65,8 @@ export interface CountWorkflowExecution { export type WorkflowExecutionDescription = Replace< WorkflowExecutionInfo, { + staticSummary?: string; + staticDetails?: string; raw: DescribeWorkflowExecutionResponse; } >; diff --git a/packages/client/src/workflow-client.ts b/packages/client/src/workflow-client.ts index 15b75ce3e..0ce1bd08d 100644 --- a/packages/client/src/workflow-client.ts +++ b/packages/client/src/workflow-client.ts @@ -22,6 +22,7 @@ import { decodeRetryState, encodeWorkflowIdConflictPolicy, WorkflowIdConflictPolicy, + JsonPayloadConverter, } from '@temporalio/common'; import { encodeUnifiedSearchAttributes } from '@temporalio/common/lib/converter/payload-search-attributes'; import { composeInterceptors } from '@temporalio/common/lib/interceptors'; @@ -510,7 +511,7 @@ export class WorkflowClient extends BaseClient { protected async _start( workflowTypeOrFunc: string | T, - options: WithWorkflowArgs, + options: WorkflowStartOptions, interceptors: WorkflowClientInterceptor[] ): Promise { const workflowType = extractWorkflowType(workflowTypeOrFunc); @@ -1196,6 +1197,7 @@ export class WorkflowClient extends BaseClient { protected async _signalWithStartWorkflowHandler(input: WorkflowSignalWithStartInput): Promise { const { identity } = this.options; const { options, workflowType, signalName, signalArgs, headers } = input; + const jsonConverter = new JsonPayloadConverter(); const req: temporal.api.workflowservice.v1.ISignalWithStartWorkflowExecutionRequest = { namespace: this.options.namespace, identity, @@ -1225,6 +1227,10 @@ export class WorkflowClient extends BaseClient { : undefined, cronSchedule: options.cronSchedule, header: { fields: headers }, + userMetadata: { + summary: jsonConverter.toPayload(options?.staticSummary), + details: jsonConverter.toPayload(options?.staticDetails), + }, }; try { return (await this.workflowService.signalWithStartWorkflowExecution(req)).runId; @@ -1265,7 +1271,7 @@ export class WorkflowClient extends BaseClient { protected async createStartWorkflowRequest(input: WorkflowStartInput): Promise { const { options: opts, workflowType, headers } = input; const { identity, namespace } = this.options; - + const jsonConverter = new JsonPayloadConverter(); return { namespace, identity, @@ -1293,6 +1299,10 @@ export class WorkflowClient extends BaseClient { : undefined, cronSchedule: opts.cronSchedule, header: { fields: headers }, + userMetadata: { + summary: jsonConverter.toPayload(opts?.staticSummary), + details: jsonConverter.toPayload(opts?.staticDetails), + }, }; } @@ -1426,8 +1436,12 @@ export class WorkflowClient extends BaseClient { workflowExecution: { workflowId, runId }, }); const info = await executionInfoFromRaw(raw.workflowExecutionInfo ?? {}, this.client.dataConverter, raw); + const jsonConverter = new JsonPayloadConverter(); + const userMetadata = raw.executionConfig?.userMetadata; return { ...info, + staticDetails: userMetadata?.details ? jsonConverter.fromPayload(userMetadata.details) : undefined, + staticSummary: userMetadata?.summary ? jsonConverter.fromPayload(userMetadata.summary) : undefined, raw, }; }, diff --git a/packages/common/src/workflow-options.ts b/packages/common/src/workflow-options.ts index 6206a9861..4868e6292 100644 --- a/packages/common/src/workflow-options.ts +++ b/packages/common/src/workflow-options.ts @@ -190,6 +190,21 @@ export interface BaseWorkflowOptions { * by {@link typedSearchAttributes}. */ typedSearchAttributes?: SearchAttributePair[] | TypedSearchAttributes; + + /** + * General fixed details for this workflow execution that may appear in UI/CLI. + * This can be in Temporal markdown format and can span multiple lines. + * + * @experimental + */ + staticDetails?: string; + /** + * A single-line fixed summary for this workflow execution that may appear in the UI/CLI. + * This can be in single-line Temporal markdown format. + * + * @experimental + */ + staticSummary?: string; } export type WithWorkflowArgs = T & diff --git a/packages/test/src/test-integration-workflows.ts b/packages/test/src/test-integration-workflows.ts index 9d55c87ef..3c96e536e 100644 --- a/packages/test/src/test-integration-workflows.ts +++ b/packages/test/src/test-integration-workflows.ts @@ -8,17 +8,19 @@ import { msToNumber, tsToMs } from '@temporalio/common/lib/time'; import { TestWorkflowEnvironment } from '@temporalio/testing'; import { CancelReason } from '@temporalio/worker/lib/activity'; import * as workflow from '@temporalio/workflow'; -import { defineQuery, defineSignal } from '@temporalio/workflow'; +import { defineQuery, defineSignal, setHandler } from '@temporalio/workflow'; import { SdkFlags } from '@temporalio/workflow/lib/flags'; import { ActivityCancellationType, ApplicationFailure, defineSearchAttributeKey, + JsonPayloadConverter, SearchAttributePair, SearchAttributeType, TypedSearchAttributes, WorkflowExecutionAlreadyStartedError, } from '@temporalio/common'; +import { temporal } from '@temporalio/proto'; import { signalSchedulingWorkflow } from './activities/helpers'; import { activityStartedSignal } from './workflows/definitions'; import * as workflows from './workflows'; @@ -1337,3 +1339,76 @@ test('can register search attributes to dev server', async (t) => { t.deepEqual(desc.searchAttributes, { 'new-search-attr': [12] }); // eslint-disable-line deprecation/deprecation await env.teardown(); }); + +export async function userMetadataWorkflow(): Promise { + let done = false; + const signalDef = defineSignal('done'); + setHandler(signalDef, () => { + done = true; + }); + + // That workflow should call an activity (with summary) + const { activityWithSummary } = workflow.proxyActivities({ scheduleToCloseTimeout: '10s' }).withSummaries({ + activityWithSummary: 'activity summary', + }); + await activityWithSummary(); + // Should have a timer (with summary) + await workflow.sleep(5, 'timer summary'); + // Set current details + workflow.setCurrentDetails('current wf details'); + // Unblock on var -> query current details (or return) + await workflow.condition(() => done); + return workflow.getCurrentDetails(); +} + +test('User metadata on workflow, timer, activity', async (t) => { + const { createWorker, startWorkflow } = helpers(t); + const worker = await createWorker({ + activities: { + async activityWithSummary() {}, + }, + }); + + await worker.runUntil(async () => { + // Start a workflow with static details + const handle = await startWorkflow(userMetadataWorkflow, { + staticSummary: 'wf static summary', + staticDetails: 'wf static details', + }); + // Describe workflow -> static summary, static details + const desc = await handle.describe(); + t.true(desc.staticSummary === 'wf static summary'); + t.true(desc.staticDetails === 'wf static details'); + + await handle.signal('done'); + const res = await handle.result(); + t.true(res === 'current wf details'); + + // Get history events for timer and activity summaries. + const resp = await t.context.env.client.workflowService.getWorkflowExecutionHistory({ + namespace: t.context.env.client.options.namespace, + execution: { + workflowId: handle.workflowId, + runId: handle.firstExecutionRunId, + }, + }); + const jsonConverter = new JsonPayloadConverter(); + for (const event of resp.history?.events ?? []) { + if (event.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED) { + t.deepEqual(jsonConverter.fromPayload(event.userMetadata?.summary ?? {}), 'wf static summary'); + t.deepEqual(jsonConverter.fromPayload(event.userMetadata?.details ?? {}), 'wf static details'); + } else if (event.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED) { + t.deepEqual(jsonConverter.fromPayload(event.userMetadata?.summary ?? {}), 'activity summary'); + } else if (event.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_TIMER_STARTED) { + t.deepEqual(jsonConverter.fromPayload(event.userMetadata?.summary ?? {}), 'timer summary'); + } + } + + // Run metadata query -> get current details + const wfMetadata = (await handle.query('__temporal_workflow_metadata')) as temporal.api.sdk.v1.IWorkflowMetadata; + t.deepEqual(wfMetadata.definition?.signalDefinitions?.length, 1); + t.deepEqual(wfMetadata.definition?.signalDefinitions?.[0].name, 'done'); + t.deepEqual(wfMetadata.definition?.queryDefinitions?.length, 3); // default queries + t.deepEqual(wfMetadata.currentDetails, 'current wf details'); + }); +}); diff --git a/packages/test/src/test-schedules.ts b/packages/test/src/test-schedules.ts index 5f1c0d96a..b829dc177 100644 --- a/packages/test/src/test-schedules.ts +++ b/packages/test/src/test-schedules.ts @@ -881,4 +881,31 @@ if (RUN_INTEGRATION_TESTS) { await handle.delete(); } }); + + test.serial('User metadata on schedule', async (t) => { + const { client } = t.context; + const scheduleId = `schedule-with-user-metadata-${randomUUID()}`; + const handle = await client.schedule.create({ + scheduleId, + spec: {}, + action: { + type: 'startWorkflow', + workflowType: dummyWorkflow, + taskQueue, + staticSummary: 'schedule static summary', + staticDetails: 'schedule static details', + }, + }); + + try { + const describedSchedule = await handle.describe(); + t.deepEqual(describedSchedule.spec.calendars, []); + t.deepEqual(describedSchedule.spec.intervals, []); + t.deepEqual(describedSchedule.spec.skip, []); + t.deepEqual(describedSchedule.action.staticSummary, 'schedule static summary'); + t.deepEqual(describedSchedule.action.staticDetails, 'schedule static details'); + } finally { + await handle.delete(); + } + }); } diff --git a/packages/workflow/src/interceptors.ts b/packages/workflow/src/interceptors.ts index 1ae70c4b5..5ee9e046a 100644 --- a/packages/workflow/src/interceptors.ts +++ b/packages/workflow/src/interceptors.ts @@ -80,6 +80,7 @@ export interface ActivityInput { readonly options: ActivityOptions; readonly headers: Headers; readonly seq: number; + readonly cmdOpts?: WorkflowCommandOptions; } /** Input for WorkflowOutboundCallsInterceptor.scheduleLocalActivity */ @@ -91,6 +92,7 @@ export interface LocalActivityInput { readonly seq: number; readonly originalScheduleTime?: Timestamp; readonly attempt: number; + readonly cmdOpts?: WorkflowCommandOptions; } /** Input for WorkflowOutboundCallsInterceptor.startChildWorkflowExecution */ @@ -101,10 +103,33 @@ export interface StartChildWorkflowExecutionInput { readonly seq: number; } +/** + * User metadata that can be attached to workflow commands. + * + * Current used for: + * - startTimer, scheduleActivity/scheduleLocalActivity commands + * - internal metadata query + */ +export interface UserMetadata { + /** @experimental A single line summary of the command's purpose */ + summary?: string; + /** @experimental Additional details about the command for longer-text description, can span multiple lines */ + details?: string; +} + +/** + * Options that can be attached to workflow commands. + */ +export interface WorkflowCommandOptions { + /** User metadata for the command that may be persisted to history */ + readonly userMetadata?: UserMetadata; +} + /** Input for WorkflowOutboundCallsInterceptor.startTimer */ export interface TimerInput { readonly durationMs: number; readonly seq: number; + readonly cmdOpts?: WorkflowCommandOptions; } /** diff --git a/packages/workflow/src/internals.ts b/packages/workflow/src/internals.ts index acc49985b..b044104e9 100644 --- a/packages/workflow/src/internals.ts +++ b/packages/workflow/src/internals.ts @@ -313,6 +313,7 @@ export class Activator implements ActivationHandler { signalDefinitions, updateDefinitions, }, + currentDetails: this.currentDetails, }; }, description: 'Returns metadata associated with this workflow.', @@ -416,6 +417,8 @@ export class Activator implements ActivationHandler { public readonly registeredActivityNames: Set; + public currentDetails: string = ''; + constructor({ info, now, diff --git a/packages/workflow/src/workflow.ts b/packages/workflow/src/workflow.ts index 711b71b04..70177b662 100644 --- a/packages/workflow/src/workflow.ts +++ b/packages/workflow/src/workflow.ts @@ -22,6 +22,7 @@ import { WorkflowReturnType, WorkflowUpdateValidatorType, SearchAttributeUpdatePair, + JsonPayloadConverter, } from '@temporalio/common'; import { encodeUnifiedSearchAttributes, @@ -39,6 +40,8 @@ import { SignalWorkflowInput, StartChildWorkflowExecutionInput, TimerInput, + UserMetadata, + WorkflowCommandOptions, } from './interceptors'; import { ChildWorkflowCancellationType, @@ -81,6 +84,27 @@ export function addDefaultWorkflowOptions( }; } +function addUserMetadata(userMetadata?: UserMetadata): temporal.api.sdk.v1.IUserMetadata | undefined { + if (userMetadata == null) { + return undefined; + } + + const jsonConverter = new JsonPayloadConverter(); + return { + summary: jsonConverter.toPayload(userMetadata.summary), + details: jsonConverter.toPayload(userMetadata.details), + }; +} + +function addWorkflowCommandOptions(cmdOpts?: WorkflowCommandOptions): object { + if (cmdOpts == null) { + return {}; + } + return { + userMetadata: addUserMetadata(cmdOpts.userMetadata), + }; +} + /** * Push a startTimer command into state accumulator and register completion */ @@ -112,6 +136,7 @@ function timerNextHandler(input: TimerInput) { seq: input.seq, startToFireTimeout: msToTs(input.durationMs), }, + ...addWorkflowCommandOptions(input.cmdOpts), }); activator.completions.timer.set(input.seq, { resolve, @@ -127,8 +152,9 @@ function timerNextHandler(input: TimerInput) { * * @param ms sleep duration - number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}. * If given a negative number or 0, value will be set to 1. + * @param summary a short summary/description of the timer. Can serve as a timer ID. */ -export function sleep(ms: Duration): Promise { +export function sleep(ms: Duration, summary?: string): Promise { const activator = assertInWorkflowContext('Workflow.sleep(...) may only be used from a Workflow Execution'); const seq = activator.nextSeqs.timer++; @@ -139,6 +165,7 @@ export function sleep(ms: Duration): Promise { return execute({ durationMs, seq, + ...(summary !== undefined && { cmdOpts: { userMetadata: { summary } } }), }); } @@ -154,7 +181,14 @@ const validateLocalActivityOptions = validateActivityOptions; /** * Push a scheduleActivity command into activator accumulator and register completion */ -function scheduleActivityNextHandler({ options, args, headers, seq, activityType }: ActivityInput): Promise { +function scheduleActivityNextHandler({ + options, + args, + headers, + seq, + activityType, + cmdOpts, +}: ActivityInput): Promise { const activator = getActivator(); validateActivityOptions(options); return new Promise((resolve, reject) => { @@ -194,6 +228,7 @@ function scheduleActivityNextHandler({ options, args, headers, seq, activityType doNotEagerlyExecute: !(options.allowEagerDispatch ?? true), versioningIntent: versioningIntentToProto(options.versioningIntent), }, + ...addWorkflowCommandOptions(cmdOpts), }); activator.completions.activity.set(seq, { resolve, @@ -213,6 +248,7 @@ async function scheduleLocalActivityNextHandler({ activityType, attempt, originalScheduleTime, + cmdOpts, }: LocalActivityInput): Promise { const activator = getActivator(); // Eagerly fail the local activity (which will in turn fail the workflow task. @@ -259,6 +295,7 @@ async function scheduleLocalActivityNextHandler({ headers, cancellationType: encodeActivityCancellationType(options.cancellationType), }, + ...addWorkflowCommandOptions(cmdOpts), }); activator.completions.activity.set(seq, { resolve, @@ -271,7 +308,12 @@ async function scheduleLocalActivityNextHandler({ * Schedule an activity and run outbound interceptors * @hidden */ -export function scheduleActivity(activityType: string, args: any[], options: ActivityOptions): Promise { +export function scheduleActivity( + activityType: string, + args: any[], + options: ActivityOptions, + summary?: string +): Promise { const activator = assertInWorkflowContext( 'Workflow.scheduleActivity(...) may only be used from a Workflow Execution' ); @@ -287,6 +329,7 @@ export function scheduleActivity(activityType: string, args: any[], options: options, args, seq, + ...(summary !== undefined && { cmdOpts: { userMetadata: { summary } } }), }) as Promise; } @@ -297,7 +340,8 @@ export function scheduleActivity(activityType: string, args: any[], options: export async function scheduleLocalActivity( activityType: string, args: any[], - options: LocalActivityOptions + options: LocalActivityOptions, + summary?: string ): Promise { const activator = assertInWorkflowContext( 'Workflow.scheduleLocalActivity(...) may only be used from a Workflow Execution' @@ -326,6 +370,7 @@ export async function scheduleLocalActivity( seq, attempt, originalScheduleTime, + ...(summary !== undefined && { cmdOpts: { userMetadata: { summary } } }), })) as Promise; } catch (err) { if (err instanceof LocalActivityDoBackoff) { @@ -499,6 +544,18 @@ export type ActivityInterfaceFor = { [K in keyof T]: T[K] extends ActivityFunction ? T[K] : typeof NotAnActivityMethod; }; +/** + * Extends ActivityInterfaceFor to include the withSummaries method + */ +export type ActivityInterfaceWithSummaries = ActivityInterfaceFor & { + /** + * Provide descriptive summaries for activities + * @param summaries Record mapping activity names to their summary descriptions + * @returns A new proxy with the provided summaries + */ + withSummaries(summaries: Record): ActivityInterfaceFor; +}; + /** * Configure Activity functions with given {@link ActivityOptions}. * @@ -517,6 +574,19 @@ export type ActivityInterfaceFor = { * startToCloseTimeout: '30 minutes', * }); * + * // Setup Activities with summaries for better observability + * const { + * httpGet, + * processData, + * saveResults + * } = proxyActivities({ + * startToCloseTimeout: '10m', + * }).withSummaries({ + * httpGet: 'Fetches data from external API', + * processData: 'Processes the fetched data', + * saveResults: 'Saves processed results to database' + * }); + * * // Setup Activities from an explicit interface (e.g. when defined by another SDK) * interface JavaActivities { * httpGetFromJava(url: string): Promise @@ -537,25 +607,35 @@ export type ActivityInterfaceFor = { * } * ``` */ -export function proxyActivities(options: ActivityOptions): ActivityInterfaceFor { +export function proxyActivities(options: ActivityOptions): ActivityInterfaceWithSummaries { if (options === undefined) { throw new TypeError('options must be defined'); } // Validate as early as possible for immediate user feedback validateActivityOptions(options); - return new Proxy( - {}, - { - get(_, activityType) { - if (typeof activityType !== 'string') { - throw new TypeError(`Only strings are supported for Activity types, got: ${String(activityType)}`); + + function createActivityProxy(summaries: Record = {}): ActivityInterfaceWithSummaries { + return new Proxy({} as ActivityInterfaceWithSummaries, { + get(_, prop) { + if (prop === 'withSummaries') { + return function withSummaries(newSummaries: Record): ActivityInterfaceWithSummaries { + return createActivityProxy(newSummaries); + }; } + + if (typeof prop !== 'string') { + throw new TypeError(`Only strings are supported for Activity types, got: ${String(prop)}`); + } + return function activityProxyFunction(...args: unknown[]): Promise { - return scheduleActivity(activityType, args, options); + const summary = summaries[prop]; + return scheduleActivity(prop, args, options, summary); }; }, - } - ) as any; + }); + } + + return createActivityProxy(); } /** @@ -568,25 +648,37 @@ export function proxyActivities(options: ActivityOptions) * * @see {@link proxyActivities} for examples */ -export function proxyLocalActivities(options: LocalActivityOptions): ActivityInterfaceFor { +export function proxyLocalActivities( + options: LocalActivityOptions +): ActivityInterfaceWithSummaries { if (options === undefined) { throw new TypeError('options must be defined'); } // Validate as early as possible for immediate user feedback validateLocalActivityOptions(options); - return new Proxy( - {}, - { - get(_, activityType) { - if (typeof activityType !== 'string') { - throw new TypeError(`Only strings are supported for Activity types, got: ${String(activityType)}`); + + function createLocalActivityProxy(summaries: Record = {}): ActivityInterfaceWithSummaries { + return new Proxy({} as ActivityInterfaceWithSummaries, { + get(_, prop) { + if (prop === 'withSummaries') { + return function withSummaries(newSummaries: Record): ActivityInterfaceWithSummaries { + return createLocalActivityProxy(newSummaries); + }; } - return function localActivityProxyFunction(...args: unknown[]) { - return scheduleLocalActivity(activityType, args, options); + + if (typeof prop !== 'string') { + throw new TypeError(`Only strings are supported for Activity types, got: ${String(prop)}`); + } + + return function localActivityProxyFunction(...args: unknown[]): Promise { + const summary = summaries[prop]; + return scheduleLocalActivity(prop, args, options, summary); }; }, - } - ) as any; + }); + } + + return createLocalActivityProxy(); } // TODO: deprecate this patch after "enough" time has passed @@ -956,13 +1048,13 @@ export function makeContinueAsNewFunc( * @example * * ```ts - *import { continueAsNew } from '@temporalio/workflow'; -import { SearchAttributeType } from '@temporalio/common'; + * import { continueAsNew } from '@temporalio/workflow'; + * import { SearchAttributeType } from '@temporalio/common'; * - *export async function myWorkflow(n: number): Promise { - * // ... Workflow logic - * await continueAsNew(n + 1); - *} + * export async function myWorkflow(n: number): Promise { + * // ... Workflow logic + * await continueAsNew(n + 1); + * } * ``` */ export function continueAsNew(...args: Parameters): Promise { @@ -1590,3 +1682,13 @@ export function allHandlersFinished(): boolean { export const stackTraceQuery = defineQuery('__stack_trace'); export const enhancedStackTraceQuery = defineQuery('__enhanced_stack_trace'); export const workflowMetadataQuery = defineQuery('__temporal_workflow_metadata'); + +export function getCurrentDetails(): string { + const activator = assertInWorkflowContext('getCurrentDetails() may only be used from a Workflow Execution.'); + return activator.currentDetails; +} + +export function setCurrentDetails(details: string): void { + const activator = assertInWorkflowContext('getCurrentDetails() may only be used from a Workflow Execution.'); + activator.currentDetails = details; +}