Skip to content

User metadata #1657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
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
8 changes: 6 additions & 2 deletions packages/client/src/schedule-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
extractWorkflowType,
LoadedDataConverter,
} from '@temporalio/common';
import { encodeUserMetadata, decodeUserMetadata } from '@temporalio/common/lib/user-metadata';
import {
encodeUnifiedSearchAttributes,
decodeSearchAttributes,
Expand Down Expand Up @@ -196,8 +197,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: {
Expand Down Expand Up @@ -270,6 +270,7 @@ export async function encodeScheduleAction(
}
: undefined,
header: { fields: headers },
userMetadata: await encodeUserMetadata(dataConverter, action.staticSummary, action.staticDetails),
priority: action.priority ? compilePriority(action.priority) : undefined,
},
};
Expand Down Expand Up @@ -320,6 +321,7 @@ export async function decodeScheduleAction(
pb: temporal.api.schedule.v1.IScheduleAction
): Promise<ScheduleDescriptionAction> {
if (pb.startWorkflow) {
const { staticSummary, staticDetails } = await decodeUserMetadata(dataConverter, pb.startWorkflow?.userMetadata);
return {
type: 'startWorkflow',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand All @@ -336,6 +338,8 @@ export async function decodeScheduleAction(
workflowExecutionTimeout: optionalTsToMs(pb.startWorkflow.workflowExecutionTimeout),
workflowRunTimeout: optionalTsToMs(pb.startWorkflow.workflowRunTimeout),
workflowTaskTimeout: optionalTsToMs(pb.startWorkflow.workflowTaskTimeout),
staticSummary,
staticDetails,
priority: decodePriority(pb.startWorkflow.priority),
};
}
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/schedule-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,8 @@ export type ScheduleOptionsStartWorkflowAction<W extends Workflow> = {
| 'workflowExecutionTimeout'
| 'workflowRunTimeout'
| 'workflowTaskTimeout'
| 'staticDetails'
| 'staticSummary'
> & {
/**
* Workflow id to use when starting. Assign a meaningful business id.
Expand Down Expand Up @@ -815,6 +817,8 @@ export type ScheduleDescriptionStartWorkflowAction = ScheduleSummaryStartWorkflo
| 'workflowExecutionTimeout'
| 'workflowRunTimeout'
| 'workflowTaskTimeout'
| 'staticSummary'
| 'staticDetails'
| 'priority'
>;

Expand Down
18 changes: 17 additions & 1 deletion packages/client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,23 @@ export type WorkflowExecutionDescription = Replace<
{
raw: DescribeWorkflowExecutionResponse;
}
>;
> & {
/**
* 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 User metadata is a new API and suspectible to change.
*/
staticDetails: () => Promise<string | undefined>;

/**
* 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 User metadata is a new API and suspectible to change.
*/
staticSummary: () => Promise<string | undefined>;
};

export type WorkflowService = proto.temporal.api.workflowservice.v1.WorkflowService;
export const { WorkflowService } = proto.temporal.api.workflowservice.v1;
Expand Down
12 changes: 10 additions & 2 deletions packages/client/src/workflow-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
WorkflowIdConflictPolicy,
compilePriority,
} from '@temporalio/common';
import { encodeUserMetadata } from '@temporalio/common/lib/user-metadata';
import { encodeUnifiedSearchAttributes } from '@temporalio/common/lib/converter/payload-search-attributes';
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
import { History } from '@temporalio/common/lib/proto-utils';
Expand All @@ -32,6 +33,7 @@ import {
decodeArrayFromPayloads,
decodeFromPayloadsAtIndex,
decodeOptionalFailureToOptionalError,
decodeOptionalSinglePayload,
encodeMapToPayloads,
encodeToPayloads,
} from '@temporalio/common/lib/internal-non-workflow';
Expand Down Expand Up @@ -511,7 +513,7 @@ export class WorkflowClient extends BaseClient {

protected async _start<T extends Workflow>(
workflowTypeOrFunc: string | T,
options: WithWorkflowArgs<T, WorkflowOptions>,
options: WorkflowStartOptions<T>,
interceptors: WorkflowClientInterceptor[]
): Promise<string> {
const workflowType = extractWorkflowType(workflowTypeOrFunc);
Expand Down Expand Up @@ -1226,6 +1228,7 @@ export class WorkflowClient extends BaseClient {
: undefined,
cronSchedule: options.cronSchedule,
header: { fields: headers },
userMetadata: await encodeUserMetadata(this.dataConverter, options.staticSummary, options.staticDetails),
priority: options.priority ? compilePriority(options.priority) : undefined,
versioningOverride: options.versioningOverride ?? undefined,
};
Expand Down Expand Up @@ -1268,7 +1271,6 @@ export class WorkflowClient extends BaseClient {
protected async createStartWorkflowRequest(input: WorkflowStartInput): Promise<StartWorkflowExecutionRequest> {
const { options: opts, workflowType, headers } = input;
const { identity, namespace } = this.options;

return {
namespace,
identity,
Expand Down Expand Up @@ -1296,6 +1298,7 @@ export class WorkflowClient extends BaseClient {
: undefined,
cronSchedule: opts.cronSchedule,
header: { fields: headers },
userMetadata: await encodeUserMetadata(this.dataConverter, opts.staticSummary, opts.staticDetails),
priority: opts.priority ? compilePriority(opts.priority) : undefined,
versioningOverride: opts.versioningOverride ?? undefined,
};
Expand Down Expand Up @@ -1431,8 +1434,13 @@ export class WorkflowClient extends BaseClient {
workflowExecution: { workflowId, runId },
});
const info = await executionInfoFromRaw(raw.workflowExecutionInfo ?? {}, this.client.dataConverter, raw);
const userMetadata = raw.executionConfig?.userMetadata;
return {
...info,
staticDetails: async () =>
(await decodeOptionalSinglePayload(this.client.dataConverter, userMetadata?.details)) ?? undefined,
staticSummary: async () =>
(await decodeOptionalSinglePayload(this.client.dataConverter, userMetadata?.summary)) ?? undefined,
raw,
};
},
Expand Down
16 changes: 16 additions & 0 deletions packages/common/src/activity-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ export interface ActivityOptions {
*/
versioningIntent?: VersioningIntent;

/**
* A fixed, 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 User metadata is a new API and suspectible to change.
*/
summary?: string;

/**
* Priority of this activity
*/
Expand Down Expand Up @@ -192,4 +200,12 @@ export interface LocalActivityOptions {
* - `ABANDON` - Do not request cancellation of the activity and immediately report cancellation to the workflow.
*/
cancellationType?: ActivityCancellationType;

/**
* A fixed, 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 User metadata is a new API and suspectible to change.
*/
summary?: string;
}
25 changes: 24 additions & 1 deletion packages/common/src/internal-non-workflow/codec-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Payload } from '../interfaces';
import { arrayFromPayloads, fromPayloadsAtIndex, toPayloads } from '../converter/payload-converter';
import { arrayFromPayloads, fromPayloadsAtIndex, PayloadConverter, toPayloads } from '../converter/payload-converter';
import { PayloadConverterError } from '../errors';
import { PayloadCodec } from '../converter/payload-codec';
import { ProtoFailure } from '../failure';
Expand Down Expand Up @@ -72,6 +72,17 @@ export async function decodeOptionalSingle(
return await decodeSingle(codecs, payload);
}

/** Run {@link PayloadCodec.decode} and convert from a single Payload */
export async function decodeOptionalSinglePayload<T>(
dataConverter: LoadedDataConverter,
payload?: Payload | null | undefined
): Promise<T | null | undefined> {
const { payloadConverter, payloadCodecs } = dataConverter;
const decoded = await decodeOptionalSingle(payloadCodecs, payload);
if (decoded == null) return decoded;
return payloadConverter.fromPayload(decoded);
}

/**
* Run {@link PayloadConverter.toPayload} on value, and then encode it.
*/
Expand All @@ -80,6 +91,18 @@ export async function encodeToPayload(converter: LoadedDataConverter, value: unk
return await encodeSingle(payloadCodecs, payloadConverter.toPayload(value));
}

/**
* Run {@link PayloadConverter.toPayload} on an optional value, and then encode it.
*/
export function encodeOptionalToPayload(
payloadConverter: PayloadConverter,
value: unknown
): Payload | null | undefined {
if (value == null) return value;

return payloadConverter.toPayload(value);
}

/**
* Decode `payloads` and then return {@link arrayFromPayloads}`.
*/
Expand Down
30 changes: 30 additions & 0 deletions packages/common/src/internal-workflow/objects-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,33 @@ export function mergeObjects<T extends Record<string, any>>(

return changed ? (merged as T) : original;
}

function isObject(item: any): item is Record<string, any> {
return item && typeof item === 'object' && !Array.isArray(item);
}

/**
* Recursively merges two objects, returning a new object.
*
* Properties from `source` will overwrite properties on `target`.
* Nested objects are merged recursively.
*
* Object fields in the returned object are references, as in,
* the returned object is not completely fresh.
*/
export function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
const output = { ...target };

if (isObject(target) && isObject(source)) {
for (const key of Object.keys(source)) {
const sourceValue = source[key];
if (isObject(sourceValue) && key in target && isObject(target[key] as any)) {
output[key as keyof T] = deepMerge(target[key], sourceValue);
} else {
(output as any)[key] = sourceValue;
}
}
}

return output;
}
66 changes: 66 additions & 0 deletions packages/common/src/user-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { temporal } from '@temporalio/proto';
import { PayloadConverter } from './converter/payload-converter';
import { LoadedDataConverter } from './converter/data-converter';
import { encodeOptionalToPayload, decodeOptionalSinglePayload, encodeOptionalSingle } from './internal-non-workflow';

/**
* User metadata that can be attached to workflow commands.
*/
export interface UserMetadata {
/** @experimental A fixed, single line summary of the command's purpose */
staticSummary?: string;
/** @experimental Fixed additional details about the command for longer-text description, can span multiple lines */
staticDetails?: string;
}

export function userMetadataToPayload(
payloadConverter: PayloadConverter,
staticSummary: string | undefined,
staticDetails: string | undefined
): temporal.api.sdk.v1.IUserMetadata | undefined {
if (staticSummary == null && staticDetails == null) return undefined;

const summary = encodeOptionalToPayload(payloadConverter, staticSummary);
const details = encodeOptionalToPayload(payloadConverter, staticDetails);

if (summary == null && details == null) return undefined;

return { summary, details };
}

export async function encodeUserMetadata(
dataConverter: LoadedDataConverter,
staticSummary: string | undefined,
staticDetails: string | undefined
): Promise<temporal.api.sdk.v1.IUserMetadata | undefined> {
if (staticSummary == null && staticDetails == null) return undefined;

const { payloadConverter, payloadCodecs } = dataConverter;
const summary = await encodeOptionalSingle(
payloadCodecs,
await encodeOptionalToPayload(payloadConverter, staticSummary)
);
const details = await encodeOptionalSingle(
payloadCodecs,
await encodeOptionalToPayload(payloadConverter, staticDetails)
);

if (summary == null && details == null) return undefined;

return { summary, details };
}

export async function decodeUserMetadata(
dataConverter: LoadedDataConverter,
metadata: temporal.api.sdk.v1.IUserMetadata | undefined | null
): Promise<UserMetadata> {
const res = { staticSummary: undefined, staticDetails: undefined };
if (metadata == null) return res;

const staticSummary = (await decodeOptionalSinglePayload<string>(dataConverter, metadata.summary)) ?? undefined;
const staticDetails = (await decodeOptionalSinglePayload<string>(dataConverter, metadata.details)) ?? undefined;

if (staticSummary == null && staticDetails == null) return res;

return { staticSummary, staticDetails };
}
15 changes: 15 additions & 0 deletions packages/common/src/workflow-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,21 @@ export interface BaseWorkflowOptions {
*/
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 User metadata is a new API and suspectible to change.
*/
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 User metadata is a new API and suspectible to change.
*/
staticSummary?: string;

/**
* Priority of a workflow
*/
Expand Down
Loading
Loading