0% found this document useful (0 votes)
78 views

TypeScript SDK Developer's Guide - Features - Temporal Documentation

Uploaded by

Alias D
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
78 views

TypeScript SDK Developer's Guide - Features - Temporal Documentation

Uploaded by

Alias D
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 41

02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

Dev guide TypeScript Features


TypeScript SDK developer's guide - Features
The Features section of the Temporal Developer's guide provides basic implementation
guidance on how to use many of the development features available to Workflows and
Activities in the Temporal Platform.
In this section you can find the following:
How to develop Signals
How to develop Queries
How to start a Child Workflow Execution
How to start a Temporal Cron Job
How to use Continue-As-New
How to set Workflow timeouts & retries
How to set Activity timeouts & retries
How to Heartbeat an Activity
How to Asynchronously complete an Activity
How to register Namespaces
How to use custom payload conversion

How to develop with Signals


A Signal is a message sent to a running Workflow Execution.
Signals are defined in your code and handled in your Workflow Definition. Signals can be sent
to Workflow Executions from a Temporal Client or from another Workflow Execution.
How to define a Signal

A Signal has a name and can have arguments.


The name, also called a Signal type, is a string.
The arguments must be serializable .
defineSignal

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 1/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

TypeScript JavaScript

import { defineSignal } from '@temporalio/workflow';

interface JoinInput {
userId: string;
groupId: string;
}

export const joinSignal = defineSignal<[JoinInput]>('join');

How to handle a Signal

Workflows listen for Signals by the Signal's name.


setHandler

TypeScript JavaScript

import { setHandler } from '@temporalio/workflow';

export async function yourWorkflow() {


const groups = new Map<string, Set<string>>();

setHandler(joinSignal, ({ userId, groupId }: JoinInput) => {


const group = groups.get(groupId);
if (group) {
group.add(userId);
} else {
groups.set(groupId, new Set([userId]));
}
});
}

How to send a Signal from a Temporal Client

When a Signal is sent successfully from the Temporal Client, the WorkflowExecutionSignaled
Event appears in the Event History of the Workflow that receives the Signal.
WorkflowHandle.signal
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 2/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

import { WorkflowClient } from '@temporalio/client';


import { joinSignal } from './workflows';

const client = new WorkflowClient();

const handle = client.getHandle('workflow-id-123');

await handle.signal(joinSignal, { userId: 'user-1', groupId: 'group-1'


});

How to send a Signal from a Workflow

A Workflow can send a Signal to another Workflow, in which case it's called an External Signal.
When an External Signal is sent:
A SignalExternalWorkflowExecutionInitiated Event appears in the sender's Event History.
A WorkflowExecutionSignaled Event appears in the recipient's Event History.
getExternalWorkflowHandle

import { getExternalWorkflowHandle } from '@temporalio/workflow';


import { joinSignal } from './other-workflow';

export async function yourWorkflowThatSignals() {


const handle = getExternalWorkflowHandle('workflow-id-123');
await handle.signal(joinSignal, { userId: 'user-1', groupId: 'group-
1' });
}

How to Signal-With-Start

Signal-With-Start is used from the Client. It takes a Workflow Id, Workflow arguments, a Signal
name, and Signal arguments.
If there's a Workflow running with the given Workflow Id, it will be signaled. If there isn't, a new
Workflow will be started and immediately signaled.
WorkflowClient.signalWithStart

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 3/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

import { WorkflowClient } from '@temporalio/client';


import { joinSignal, yourWorkflow } from './workflows';

const client = new WorkflowClient();

await client.signalWithStart(yourWorkflow, {
workflowId: 'workflow-id-123',
args: [{ foo: 1 }],
signal: joinSignal,
signalArgs: [{ userId: 'user-1', groupId: 'group-1' }],
});

How to develop with Queries


A Query is a synchronous operation that is used to get the state of a Workflow Execution.
How to define a Query

A Query has a name and can have arguments.


The name, also called a Query type, is a string.
The arguments must be serializable .
Use defineQuery to define the name, parameters, and return value of a Query.
state/src/workflows.ts
TypeScript JavaScript

import { defineQuery } from '@temporalio/workflow';

export const getValueQuery = defineQuery<number | undefined, [string]>(


'getValue',
);

How to handle a Query

Queries are handled by your Workflow.

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 4/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

Don’t include any logic that causes Command generation within a Query handler (such as
executing Activities). Including such logic causes unexpected behavior.
Use handleQuery to handle Queries inside a Workflow.
You make a Query with handle.query(query, ...args) . A Query needs a return value, but
can also take arguments.
state/src/workflows.ts
TypeScript JavaScript

export async function trackState(): Promise<void> {


const state = new Map<string, number>();
setHandler(setValueSignal, (key, value) => void state.set(key,
value));
setHandler(getValueQuery, (key) => state.get(key));
await CancellationScope.current().cancelRequested;
}

How to send a Query

Queries are sent from a Temporal Client.


Use WorkflowHandle.query to query a running or completed Workflow.
state/src/query-workflow.ts
TypeScript JavaScript

import { Client } from '@temporalio/client';


import { getValueQuery } from './workflows';

async function run(): Promise<void> {


const client = new Client();
const handle = client.workflow.getHandle('state-id-0');
const meaning = await handle.query(getValueQuery, 'meaning-of-life');
console.log({ meaning });
}

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 5/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

How to define Signals and Queries statically or


dynamically
Handlers for both Signals and Queries can take arguments, which can be used inside
setHandler logic.

Only Signal Handlers can mutate state, and only Query Handlers can return values.
Define Signals and Queries statically

If you know the name of your Signals and Queries upfront, we recommend declaring them
outside the Workflow Definition.
signals-queries/src/workflows.ts
TypeScript JavaScript

import * as wf from '@temporalio/workflow';

export const unblockSignal = wf.defineSignal('unblock');


export const isBlockedQuery = wf.defineQuery<boolean>('isBlocked');

export async function unblockOrCancel(): Promise<void> {


let isBlocked = true;
wf.setHandler(unblockSignal, () => void (isBlocked = false));
wf.setHandler(isBlockedQuery, () => isBlocked);
console.log('Blocked');
try {
await wf.condition(() => !isBlocked);
console.log('Unblocked');
} catch (err) {
if (err instanceof wf.CancelledFailure) {
console.log('Cancelled');
}
throw err;
}
}

This technique helps provide type safety because you can export the type signature of the
Signal or Query to be called by the Client.
Define Signals and Queries dynamically
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 6/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

For more flexible use cases, you might want a dynamic Signal (such as a generated ID). You
can handle it in two ways:
Avoid making it dynamic by collapsing all Signals into one handler and move the ID to the
payload.
Actually make the Signal name dynamic by inlining the Signal definition per handler.
TypeScript JavaScript

import * as wf from '@temporalio/workflow';

// "fat handler" solution


wf.setHandler(`genericSignal`, (payload) => {
switch (payload.taskId) {
case taskAId:
// do task A things
break;
case taskBId:
// do task B things
break;
default:
throw new Error('Unexpected task.');
}
});

// "inline definition" solution


wf.setHandler(wf.defineSignal(`task-${taskAId}`), (payload) => {
/* do task A things */
});
wf.setHandler(wf.defineSignal(`task-${taskBId}`), (payload) => {
/* do task B things */
});

// utility "inline definition" helper


const inlineSignal = (signalName, handler) =>
wf.setHandler(wf.defineSignal(signalName), handler);
inlineSignal(`task-${taskBId}`, (payload) => {
/* do task B things */
});

API Design FAQs


https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 7/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

Workflow timeouts
Each Workflow timeout controls the maximum duration of a different aspect of a Workflow
Execution.
Workflow timeouts are set when starting the Workflow Execution .
Workflow Execution Timeout - restricts the maximum amount of time that a single
Workflow Execution can be executed.
Workflow Run Timeout : restricts the maximum amount of time that a single Workflow
Run can last.
Workflow Task Timeout : restricts the maximum amount of time that a Worker can
execute a Workflow Task.
Create an instance of WorkflowOptions from the Client and set your Workflow Timeout.
Available timeouts are:
workflowExecutionTimeout​

workflowRunTimeout

workflowTaskTimeout

snippets/src/client.ts
TypeScript JavaScript

await client.workflow.start(example, {
taskQueue,
workflowId,
workflowExecutionTimeout: '1 day',
});

snippets/src/client.ts
TypeScript JavaScript

await client.workflow.start(example, {
taskQueue,

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 8/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

workflowId,
workflowRunTimeout: '1 minute',
});

snippets/src/client.ts
TypeScript JavaScript

await client.workflow.start(example, {
taskQueue,
workflowId,
workflowTaskTimeout: '1 minute',
});

Workflow retries

A Retry Policy can work in cooperation with the timeouts to provide fine controls to optimize
the execution experience.
Use a Retry Policy to retry a Workflow Execution in the event of a failure.
Workflow Executions do not retry by default, and Retry Policies should be used with Workflow
Executions only in certain situations.
Create an instance of the Retry Policy, known as retry in TypeScript, from the
WorkflowOptions of the Client interface.

snippets/src/client.ts
TypeScript JavaScript

const handle = await client.workflow.start(example, {


taskQueue,
workflowId,
retry: {
maximumAttempts: 3,
},
});

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 9/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

How to set Activity timeouts


Each Activity timeout controls the maximum duration of a different aspect of an Activity
Execution.
The following timeouts are available in the Activity Options.
Schedule-To-Close Timeout : is the maximum amount of time allowed for the overall
Activity Execution .
Start-To-Close Timeout : is the maximum time allowed for a single Activity Task
Execution .
Schedule-To-Start Timeout : is the maximum amount of time that is allowed from when
an Activity Task is scheduled to when a Worker starts that Activity Task.
An Activity Execution must have either the Start-To-Close or the Schedule-To-Close Timeout
set.
When you call proxyActivities in a Workflow Function, you can set a range of
ActivityOptions .

Available timeouts are:


scheduleToCloseTimeout

startToCloseTimeout

scheduleToStartTimeout

// Sample of typical options you can set


const { greet } = proxyActivities<typeof activities>({
scheduleToCloseTimeout: '5m',
// startToCloseTimeout: "30s", // recommended
// scheduleToStartTimeout: "60s",

retry: {
// default retry policy if not specified
initialInterval: '1s',
backoffCoefficient: 2,
maximumAttempts: Infinity,
maximumInterval: 100 * initialInterval,
nonRetryableErrorTypes: [],
},
});

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 10/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

How to set an Activity Retry Policy

A Retry Policy works in cooperation with the timeouts to provide fine controls to optimize the
execution experience.
Activity Executions are automatically associated with a default Retry Policy if a custom one is
not provided.
To set Activity Retry Policies in TypeScript, pass ActivityOptions.retry to
proxyActivities .

// Sample of typical options you can set


const { yourActivity } = proxyActivities<typeof activities>({
// ...
retry: {
// default retry policy if not specified
initialInterval: '1s',
backoffCoefficient: 2,
maximumAttempts: Infinity,
maximumInterval: 100 * initialInterval,
nonRetryableErrorTypes: [],
},
});

How to Heartbeat an Activity


An Activity Heartbeat is a ping from the Worker Process that is executing the Activity to
the Temporal Cluster . Each Heartbeat informs the Temporal Cluster that the Activity
Execution is making progress and the Worker has not crashed. If the Cluster does not
receive a Heartbeat within a Heartbeat Timeout time period, the Activity will be considered
failed and another Activity Task Execution may be scheduled according to the Retry Policy.
Heartbeats may not always be sent to the Cluster—they may be throttled by the Worker.
Activity Cancellations are delivered to Activities from the Cluster when they Heartbeat.
Activities that don't Heartbeat can't receive a Cancellation. Heartbeat throttling may lead to
Cancellation getting delivered later than expected.
Heartbeats can contain a details field describing the Activity's current progress. If an
Activity gets retried, the Activity can access the details from the last Heartbeat that was
sent to the Cluster.
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 11/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

Long-running Activities should Heartbeat their progress back to the Workflow for earlier
detection of stalled Activities (with Heartbeat Timeout ) and resuming stalled Activities from
checkpoints (with Heartbeat details).
To set Activity Heartbeat, use Context.current().heartbeat() in your Activity
implementation, and set heartbeatTimeout in your Workflow.
TypeScript JavaScript

// activity implementation
export async function example(sleepIntervalMs = 1000): Promise<void> {
for (let progress = 1; progress <= 1000; ++progress) {
await Context.current().sleep(sleepIntervalMs);
// record activity heartbeat
Context.current().heartbeat();
}
}

// ...

// workflow code calling activity


const { example } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 hour',
heartbeatTimeout: '10s',
});

In the previous example, setting the Heartbeat informs the Temporal Server of the Activity's
progress at regular intervals. If the Activity stalls or the Activity Worker becomes unavailable,
the absence of Heartbeats prompts the Temporal Server to retry the Activity immediately,
without waiting for startToCloseTimeout to complete.
You can also add heartbeatDetails as a checkpoint to collect data about failures during the
execution, and use it to resume the Activity from that point.
The following example extends the previous sample to include a heartbeatDetails
checkpoint.
TypeScript JavaScript

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 12/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

export async function example(sleepIntervalMs = 1000): Promise<void> {


const startingPoint = Context.current().info.heartbeatDetails || 1;
// allow for resuming from heartbeat
for (let progress = startingPoint; progress <= 100; ++progress) {
await Context.current().sleep(sleepIntervalMs);
Context.current().heartbeat(progress);
}
}

In this example, when the heartbeatTimeout is reached and the Activity is retried, the
Activity Worker picks up the execution from where the previous attempt left off.
How to set a Heartbeat Timeout

A Heartbeat Timeout works in conjunction with Activity Heartbeats .


To set a Heartbeat Timeout, use ActivityOptions.heartbeatTimeout . If the Activity takes
longer than that between heartbeats, the Activity is failed.
// Creating a proxy for the activity.
const { longRunningActivity } = proxyActivities<typeof activities>({
// translates to 300000 ms
scheduleToCloseTimeout: '5m',
// translates to 30000 ms
startToCloseTimeout: '30s',
// equivalent to '10 seconds'
heartbeatTimeout: 10000,
});

How to asynchronously complete an Activity


Asynchronous Activity Completion enables the Activity Function to return without the
Activity Execution completing.
There are three steps to follow:
1. The Activity provides the external system with identifying information needed to complete
the Activity Execution. Identifying information can be a Task Token , or a combination of
Namespace, Workflow Id, and Activity Id.
2. The Activity Function completes in a way that identifies it as waiting to be completed by an
external system.
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 13/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

3. The Temporal Client is used to Heartbeat and complete the Activity.


To asynchronously complete an Activity, call AsyncCompletionClient.complete .
activities-examples/src/activities/async-completion.ts
TypeScript JavaScript

import { CompleteAsyncError, Context } from '@temporalio/activity';


import { AsyncCompletionClient } from '@temporalio/client';

export async function doSomethingAsync(): Promise<string> {


const taskToken = Context.current().info.taskToken;
setTimeout(() => doSomeWork(taskToken), 1000);
throw new CompleteAsyncError();
}

// this work could be done in a different process or on a different


machine
async function doSomeWork(taskToken: Uint8Array): Promise<void> {
const client = new AsyncCompletionClient();
// does some work...
await client.complete(taskToken, 'Job\'s done!');
}

Local Activities
To call Local Activities in TypeScript, use proxyLocalActivities .
TypeScript JavaScript

import * as workflow from '@temporalio/workflow';

const { getEnvVar } = workflow.proxyLocalActivities({


startToCloseTimeout: '2 seconds',
});

export async function yourWorkflow(): Promise<void> {


const someSetting = await getEnvVar('SOME_SETTING');

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 14/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

// ...
}

Local Activities must be registered with the Worker the same way non-local Activities are.

Cancel an Activity from a Workflow


Canceling an Activity from within a Workflow requires that the Activity Execution sends
Heartbeats and sets a Heartbeat Timeout. If the Heartbeat is not invoked, the Activity cannot
receive a cancellation request. When any non-immediate Activity is executed, the Activity
Execution should send Heartbeats and set a Heartbeat Timeout to ensure that the server
knows it is still working.
When an Activity is canceled, an error is raised in the Activity at the next available opportunity.
If cleanup logic needs to be performed, it can be done in a finally clause or inside a caught
cancel error. However, for the Activity to appear canceled the exception needs to be re-raised.
NOTE
Unlike regular Activities, Local Activities can be canceled if they don't send Heartbeats.
Local Activities are handled locally, and all the information needed to handle the
cancellation logic is available in the same Worker process.

How to start a Child Workflow Execution


A Child Workflow Execution is a Workflow Execution that is scheduled from within another
Workflow using a Child Workflow API.
When using a Child Workflow API, Child Workflow–related Events (such as
StartChildWorkflowExecutionInitiated , ChildWorkflowExecutionStarted , and
ChildWorkflowExecutionCompleted ) are logged in the Event History of the Child Workflow
Execution.
Always block progress until the ChildWorkflowExecutionStarted Event is logged to the Event
History to ensure the Child Workflow Execution has started. After that, Child Workflow
Executions can be abandoned by using the default Abandon Parent Close Policy set in the
Child Workflow Options.

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 15/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

To be sure that the Child Workflow Execution has started, first call the Child Workflow
Execution method on the instance of Child Workflow future, which returns a different future.
Then get the value of an object that acts as a proxy for a result that is initially unknown, which
is what waits until the Child Workflow Execution has spawned.
To start a Child Workflow Execution and return a handle to it, use startChild.
TypeScript JavaScript

import { startChild } from '@temporalio/workflow';

export async function parentWorkflow(names: string[]) {


const childHandle = await startChild(childWorkflow, {
args: [name],
// workflowId, // add business-meaningful workflow id here
// // regular workflow options apply here, with two additions
(defaults shown):
// cancellationType:
ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED,
// parentClosePolicy:
ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE
});
// you can use childHandle to signal or get result here
await childHandle.signal('anySignal');
const result = childHandle.result();
// you can use childHandle to signal, query, cancel, terminate, or
get result here
}

To start a Child Workflow Execution and await its completion, use executeChild.
By default, a child is scheduled on the same Task Queue as the parent.
child-workflows/src/workflows.ts
TypeScript JavaScript

import { executeChild } from '@temporalio/workflow';

export async function parentWorkflow(...names: string[]):


https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 16/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

Promise<string> {
const responseArray = await Promise.all(
names.map((name) =>
executeChild(childWorkflow, {
args: [name],
// workflowId, // add business-meaningful workflow id here
// // regular workflow options apply here, with two additions
(defaults shown):
// cancellationType:
ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED,
// parentClosePolicy:
ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE
})
),
);
return responseArray.join('\n');
}

To control any running Workflow from inside a Workflow, use


getExternalWorkflowHandle(workflowId).
TypeScript JavaScript

import { getExternalWorkflowHandle, workflowInfo } from


'@temporalio/workflow';

export async function terminateWorkflow() {


const { workflowId } = workflowInfo(); // no await needed
const handle = getExternalWorkflowHandle(workflowId); // sync
function, not async
await handle.cancel();
}

If the Child Workflow options aren't explicitly set, they inherit their values from the Parent
Workflow options. Two advanced options are unique to Child Workflows:
cancellationType: Controls when to throw the CanceledFailure exception when a Child
Workflow is canceled.
parentClosePolicy : Explained in the next section.

If you need to cancel a Child Workflow Execution, use cancellation scopes. A Child Workflow
Execution is automatically cancelled when its containing scope is cancelled.
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 17/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

How to set a Parent Close Policy

A Parent Close Policy determines what happens to a Child Workflow Execution if its Parent
changes to a Closed status (Completed, Failed, or Timed Out).
The default Parent Close Policy option is set to terminate the Child Workflow Execution.
To specify how a Child Workflow reacts to a Parent Workflow reaching a Closed state, use the
parentClosePolicy option.

child-workflows/src/workflows.ts
TypeScript JavaScript

import { executeChild } from '@temporalio/workflow';

export async function parentWorkflow(...names: string[]):


Promise<string> {
const responseArray = await Promise.all(
names.map((name) =>
executeChild(childWorkflow, {
args: [name],
// workflowId, // add business-meaningful workflow id here
// // regular workflow options apply here, with two additions
(defaults shown):
// cancellationType:
ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED,
// parentClosePolicy:
ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE
})
),
);
return responseArray.join('\n');
}

How to Continue-As-New
Continue-As-New enables a Workflow Execution to close successfully and create a new
Workflow Execution in a single atomic operation if the number of Events in the Event History is
becoming too large. The Workflow Execution spawned from the use of Continue-As-New has

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 18/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

the same Workflow Id, a new Run Id, and a fresh Event History and is passed all the appropriate
parameters.
To cause a Workflow Execution to Continue-As-New , the Workflow function should return
the result of the continueAsNew .
continue-as-new/src/workflows.ts
TypeScript JavaScript

import { continueAsNew, sleep } from '@temporalio/workflow';

export async function loopingWorkflow(iteration = 0): Promise<void> {


if (iteration === 10) {
return;
}
console.log('Running Workflow iteration:', iteration);
await sleep(1000);
// Must match the arguments expected by `loopingWorkflow`
await continueAsNew<typeof loopingWorkflow>(iteration + 1);
// Unreachable code, continueAsNew is like `process.exit` and will
stop execution once called.
}

Single-entity design pattern in TypeScript

The following is a simple pattern that represents a single entity. It tracks the number of
iterations regardless of frequency, and calls continueAsNew while properly handling pending
updates from Signals.
interface Input {
/* Define your Workflow input type here */
}
interface Update {
/* Define your Workflow update type here */
}

const MAX_ITERATIONS = 1;

export async function entityWorkflow(


input: Input,
isNew = true,
): Promise<void> {
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 19/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

try {
const pendingUpdates = Array<Update>();
setHandler(updateSignal, (updateCommand) => {
pendingUpdates.push(updateCommand);
});

if (isNew) {
await setup(input);
}

for (let iteration = 1; iteration <= MAX_ITERATIONS; ++iteration) {


// Ensure that we don't block the Workflow Execution forever
waiting
// for updates, which means that it will eventually Continue-As-
New
// even if it does not receive updates.
await condition(() => pendingUpdates.length > 0, '1 day');

while (pendingUpdates.length) {
const update = pendingUpdates.shift();
await runAnActivityOrChildWorkflow(update);
}
}
} catch (err) {
if (isCancellation(err)) {
await CancellationScope.nonCancellable(async () => {
await cleanup();
});
}
throw err;
}
await continueAsNew<typeof entityWorkflow>(input, false);
}

How to Schedule a Workflow


Scheduling Workflows is a crucial aspect of any automation process, especially when dealing
with time-sensitive tasks. By scheduling a Workflow, you can automate repetitive tasks, reduce
the need for manual intervention, and ensure timely execution of your business processes
Use any of the following action to help Schedule a Workflow Execution and take control over
your automation process.
How to Create a Scheduled Workflow
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 20/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

The create action enables you to create a new Schedule. When you create a new Schedule, a
unique Schedule ID is generated, which you can use to reference the Schedule in other
Schedule commands.
How to Backfill a Scheduled Workflow

The backfill action executes Actions ahead of their specified time range. This command is
useful when you need to execute a missed or delayed Action, or when you want to test the
Workflow before its scheduled time.
How to Delete a Scheduled Workflow

The delete action enables you to delete a Schedule. When you delete a Schedule, it does not
affect any Workflows that were started by the Schedule.
How to Describe a Scheduled Workflow

The describe action shows the current Schedule configuration, including information about
past, current, and future Workflow Runs. This command is helpful when you want to get a
detailed view of the Schedule and its associated Workflow Runs.
How to List a Scheduled Workflow

The list action lists all the available Schedules. This command is useful when you want to view
a list of all the Schedules and their respective Schedule IDs.
How to Pause a Scheduled Workflow

The pause action enables you to pause and unpause a Schedule. When you pause a Schedule,
all the future Workflow Runs associated with the Schedule are temporarily stopped. This
command is useful when you want to temporarily halt a Workflow due to maintenance or any
other reason.
How to Trigger a Scheduled Workflow

The trigger action triggers an immediate action with a given Schedule. By default, this action is
subject to the Overlap Policy of the Schedule. This command is helpful when you want to
execute a Workflow outside of its scheduled time.
How to Update a Scheduled Workflow
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 21/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

The update action enables you to update an existing Schedule. This command is useful when
you need to modify the Schedule's configuration, such as changing the start time, end time, or
interval.

What is a Timer?
A Workflow can set a durable timer for a fixed time period. In some SDKs, the function is called
sleep() , and in others, it's called timer() .

A Workflow can sleep for months. Timers are persisted, so even if your Worker or Temporal
Cluster is down when the time period completes, as soon as your Worker and Cluster are back
up, the sleep() call will resolve and your code will continue executing.
Sleeping is a resource-light operation: it does not tie up the process, and you can run millions
of Timers off a single Worker.

Asynchronous design patterns in TypeScript


The real value of sleep and condition is in knowing how to use them to model
asynchronous business logic. Here are some examples we use the most; we welcome more if
you can think of them!
Racing Timers

Racing Signals

Updatable Timer

How to use Temporal Cron Jobs


A Temporal Cron Job is the series of Workflow Executions that occur when a Cron Schedule
is provided in the call to spawn a Workflow Execution.
A Cron Schedule is provided as an option when the call to spawn a Workflow Execution is
made.
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 22/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

You can set each Workflow to repeat on a schedule with the cronSchedule option:
const handle = await client.start(scheduledWorkflow, {
// ...
cronSchedule: '* * * * *', // start every minute
});

How to create and manage Namespaces


You can create, update, deprecate or delete your Namespaces using either tctl or SDK APIs.
Use Namespaces to isolate your Workflow Executions according to your needs. For example,
you can use Namespaces to match the development lifecycle by having separate dev and
prod Namespaces. You could also use them to ensure Workflow Executions between different
teams never communicate - such as ensuring that the teamA Namespace never impacts the
teamB Namespace.

On Temporal Cloud, use the Temporal Cloud UI to create and manage a Namespace from the
UI, or tcld commands to manage Namespaces from the command-line interface.
On self-hosted Temporal Cluster, you can register and manage your Namespaces using tctl
(recommended) or programmatically using APIs. Note that these APIs and tctl commands will
not work with Temporal Cloud.
Use a custom Authorizer on your Frontend Service in the Temporal Cluster to set restrictions
on who can create, update, or deprecate Namespaces.
You must register a Namespace with the Temporal Cluster before setting it in the Temporal
Client.
How to register Namespaces

Registering a Namespace creates a Namespace on the Temporal Cluster or Temporal Cloud.


On Temporal Cloud, use the Temporal Cloud UI or tcld commands to create Namespaces.
On self-hosted Temporal Cluster, you can register your Namespaces using tctl (recommended)
or programmatically using APIs. Note that these APIs and tctl commands will not work with
Temporal Cloud.

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 23/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

Use a custom Authorizer on your Frontend Service in the Temporal Cluster to set restrictions
on who can create, update, or deprecate Namespaces.
How to manage Namespaces

You can get details for your Namespaces, update Namespace configuration, and deprecate or
delete your Namespaces.
On Temporal Cloud, use the Temporal Cloud UI or tcld commands to manage Namespaces.
On self-hosted Temporal Cluster, you can manage your registered Namespaces using tctl
(recommended) or programmatically using APIs. Note that these APIs and tctl commands will
not work with Temporal Cloud.
Use a custom Authorizer on your Frontend Service in the Temporal Cluster to set restrictions
on who can create, update, or deprecate Namespaces.
You must register a Namespace with the Temporal Cluster before setting it in the Temporal
Client.

How to use a custom payload converter in TypeScript


Temporal SDKs provide a Payload Converter that can be customized to convert a custom
data type to a Payload and back.
Implementing custom Payload conversion is optional. It is needed only if the default Data
Converter does not support your custom values.
To support custom Payload conversion, create a custom Payload Converter and configure
the Data Converter to use it in your Client options.
The order in which your encoding Payload Converters are applied depend on the order given to
the Data Converter. You can set multiple encoding Payload Converters to run your conversions.
When the Data Converter receives a value for conversion, it passes through each Payload
Converter in sequence until the converter that handles the data type does the conversion.
To send values that are not JSON-serializable like a BigInt or Date , provide a custom Data
Converter to the Client and Worker:
new WorkflowClient({ ..., dataConverter })
Worker.create({ ..., dataConverter })
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 24/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

A Data Converter has two parts:


Payload Converter: Sync methods that sometimes run inside the Workflow isolate (and are
thus limited).
Payload Codec: Async methods that run outside the isolate.
TypeScript JavaScript

interface DataConverter {
payloadConverterPath?: string;
payloadCodecs?: PayloadCodec[];
}

Payload Converter

API documentation: PayloadConverter


TypeScript JavaScript

interface PayloadConverter {
/**
* Converts a value to a {@link Payload}.
* @param value The value to convert. Example values include the
Workflow args sent by the client and the values returned by a Workflow
or Activity.
*/
toPayload<T>(value: T): Payload;

/**
* Converts a {@link Payload} back to a value.
*/
fromPayload<T>(payload: Payload): T;
}

Custom implementation

Some example implementations are in the SDK itself:


common/src/converter/payload-converters.ts
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 25/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

common/src/converter/protobuf-payload-converters.ts
The sample project samples-typescript/ejson creates an EJSON custom PayloadConverter .
It implements PayloadConverterWithEncoding instead of PayloadConverter so that it
could be used with CompositePayloadConverter:
ejson/src/ejson-payload-converter.ts
TypeScript JavaScript

import {
EncodingType,
METADATA_ENCODING_KEY,
Payload,
PayloadConverterError,
PayloadConverterWithEncoding,
} from '@temporalio/common';
import { decode, encode } from '@temporalio/common/lib/encoding';
import EJSON from 'ejson';

/**
* Converts between values and [EJSON]
(https://docs.meteor.com/api/ejson.html) Payloads.
*/
export class EjsonPayloadConverter implements
PayloadConverterWithEncoding {
// Use 'json/plain' so that Payloads are displayed in the UI
public encodingType = 'json/plain' as EncodingType;

public toPayload(value: unknown): Payload | undefined {


if (value === undefined) return undefined;
let ejson;
try {
ejson = EJSON.stringify(value);
} catch (e) {
throw new UnsupportedEjsonTypeError(
`Can't run EJSON.stringify on this value: ${value}. Either
convert it (or its properties) to EJSON-serializable values (see
https://docs.meteor.com/api/ejson.html ), or create a custom data
converter. EJSON.stringify error message: ${
errorMessage(
e,
)
}`,

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 26/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

e as Error,
);
}

return {
metadata: {
[METADATA_ENCODING_KEY]: encode('json/plain'),
// Include an additional metadata field to indicate that this
is an EJSON payload
format: encode('extended'),
},
data: encode(ejson),
};
}

public fromPayload<T>(content: Payload): T {


return content.data ? EJSON.parse(decode(content.data)) :
content.data;
}
}

export class UnsupportedEjsonTypeError extends PayloadConverterError {


public readonly name: string = 'UnsupportedJsonTypeError';

constructor(message: string | undefined, public readonly cause?:


Error) {
super(message ?? undefined);
}
}

Then we instantiate one and export it:


ejson/src/payload-converter.ts
TypeScript JavaScript

import {
CompositePayloadConverter,
UndefinedPayloadConverter,
} from '@temporalio/common';
import { EjsonPayloadConverter } from './ejson-payload-converter';

export const payloadConverter = new CompositePayloadConverter(


new UndefinedPayloadConverter(),

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 27/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

new EjsonPayloadConverter(),
);

We provide it to the Worker and Client:


ejson/src/worker.ts
TypeScript JavaScript

const worker = await Worker.create({


workflowsPath: require.resolve('./workflows'),
taskQueue: 'ejson',
dataConverter: {
payloadConverterPath: require.resolve('./payload-converter'),
},
});

ejson/src/client.ts
TypeScript JavaScript

const client = new Client({


dataConverter: {
payloadConverterPath: require.resolve('./payload-converter'),
},
});

Then we can use supported data types in arguments:


ejson/src/client.ts
TypeScript JavaScript

const user: User = {


id: uuid(),
// age: 1000n, BigInt isn't supported
hp: Infinity,
matcher: /.*Stormblessed/,
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 28/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

token: Uint8Array.from([1, 2, 3]),


createdAt: new Date(),
};

const handle = await client.workflow.start(example, {


args: [user],
taskQueue: 'ejson',
workflowId: `example-user-${user.id}`,
});

And they get parsed correctly for the Workflow:


ejson/src/workflows.ts
TypeScript JavaScript

import type { Result, User } from './types';

export async function example(user: User): Promise<Result> {


const success = user.createdAt.getTime() < Date.now()
&& user.hp > 50
&& user.matcher.test('Kaladin Stormblessed')
&& user.token instanceof Uint8Array;
return { success, at: new Date() };
}

Protobufs

To serialize values as Protocol Buffers (protobufs):


Use protobufjs.
Use runtime-loaded messages (not generated classes) and MessageClass.create (not
new MessageClass() ).

Generate json-module.js with a command like the following:


pbjs -t json-module -w commonjs -o protos/json-module.js
protos/*.proto

Patch json-module.js :
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 29/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

protobufs/protos/root.js
const { patchProtobufRoot } =
require('@temporalio/common/lib/protobufs');
const unpatchedRoot = require('./json-module');
module.exports = patchProtobufRoot(unpatchedRoot);

Generate root.d.ts with the following command:


pbjs -t static-module protos/*.proto | pbts -o protos/root.d.ts -

Create a DefaultPayloadConverterWithProtobufs :
protobufs/src/payload-converter.ts
TypeScript JavaScript

import { DefaultPayloadConverterWithProtobufs } from


'@temporalio/common/lib/protobufs';
import root from '../protos/root';

export const payloadConverter = new


DefaultPayloadConverterWithProtobufs({
protobufRoot: root,
});

Alternatively, we can use Protobuf Payload Converters directly, or with other converters. If we
know that we only use Protobuf objects, and we want them binary encoded (which saves
space over proto3 JSON, but can't be viewed in the Web UI), we could do the following:
TypeScript JavaScript

import { ProtobufBinaryPayloadConverter } from


'@temporalio/common/lib/protobufs';
import root from '../protos/root';

export const payloadConverter = new


ProtobufBinaryPayloadConverter(root);

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 30/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

Similarly, if we wanted binary-encoded Protobufs in addition to the other default types, we


could do the following:
TypeScript JavaScript

import {
BinaryPayloadConverter,
CompositePayloadConverter,
JsonPayloadConverter,
UndefinedPayloadConverter,
} from '@temporalio/common';
import { ProtobufBinaryPayloadConverter } from
'@temporalio/common/lib/protobufs';
import root from '../protos/root';

export const payloadConverter = new CompositePayloadConverter(


new UndefinedPayloadConverter(),
new BinaryPayloadConverter(),
new ProtobufBinaryPayloadConverter(root),
new JsonPayloadConverter(),
);

Provide it to the Worker:


protobufs/src/worker.ts
TypeScript JavaScript

const worker = await Worker.create({


workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'protobufs',
dataConverter: {
payloadConverterPath: require.resolve('./payload-converter'),
},
});

WorkerOptions.dataConverter
Provide it to the Client:
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 31/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

protobufs/src/client.ts
TypeScript JavaScript

import { Client } from '@temporalio/client';


import { v4 as uuid } from 'uuid';
import { foo, ProtoResult } from '../protos/root';
import { example } from './workflows';

async function run() {


const client = new Client({
dataConverter: {
payloadConverterPath: require.resolve('./payload-converter'),
},
});

const handle = await client.workflow.start(example, {


args: [foo.bar.ProtoInput.create({ name: 'Proto', age: 2 })],
// can't do:
// args: [new foo.bar.ProtoInput({ name: 'Proto', age: 2 })],
taskQueue: 'protobufs',
workflowId: 'my-business-id-' + uuid(),
});

console.log(`Started workflow ${handle.workflowId}`);

const result: ProtoResult = await handle.result();


console.log(result.toJSON());
}

Use protobufs in your Workflows and Activities:


protobufs/src/workflows.ts
TypeScript JavaScript

import { proxyActivities } from '@temporalio/workflow';


import { foo, ProtoResult } from '../protos/root';
import type * as activities from './activities';

const { protoActivity } = proxyActivities<typeof activities>({


startToCloseTimeout: '1 minute',
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 32/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

});

export async function example(input: foo.bar.ProtoInput):


Promise<ProtoResult> {
const result = await protoActivity(input);
return result;
}

protobufs/src/activities.ts
TypeScript JavaScript

import { foo, ProtoResult } from '../protos/root';

export async function protoActivity(


input: foo.bar.ProtoInput,
): Promise<ProtoResult> {
return ProtoResult.create({
sentence: `${input.name} is ${input.age} years old.`,
});
}

Payload Codec

API documentation: PayloadCodec


The default PayloadCodec does nothing. To create a custom one, we implement the following
interface:
TypeScript JavaScript

interface PayloadCodec {
/**
* Encode an array of {@link Payload}s for sending over the wire.
* @param payloads May have length 0.
*/
encode(payloads: Payload[]): Promise<Payload[]>;

/**
* Decode an array of {@link Payload}s received from the wire.

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 33/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

*/
decode(payloads: Payload[]): Promise<Payload[]>;
}

Encryption

Background: Encryption
The following is an example class that implements the PayloadCodec interface:
encryption/src/encryption-codec.ts
TypeScript JavaScript

import {
METADATA_ENCODING_KEY,
Payload,
PayloadCodec,
ValueError,
} from '@temporalio/common';
import { decode, encode } from '@temporalio/common/lib/encoding';
import { temporal } from '@temporalio/proto';
import { webcrypto as crypto } from 'node:crypto';
import { decrypt, encrypt } from './crypto';

const ENCODING = 'binary/encrypted';


const METADATA_ENCRYPTION_KEY_ID = 'encryption-key-id';

export class EncryptionCodec implements PayloadCodec {


constructor(
protected readonly keys: Map<string, crypto.CryptoKey>,
protected readonly defaultKeyId: string,
) {}

static async create(keyId: string): Promise<EncryptionCodec> {


const keys = new Map<string, crypto.CryptoKey>();
keys.set(keyId, await fetchKey(keyId));
return new this(keys, keyId);
}

async encode(payloads: Payload[]): Promise<Payload[]> {


return Promise.all(
payloads.map(async (payload) => ({
metadata: {

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 34/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

[METADATA_ENCODING_KEY]: encode(ENCODING),
[METADATA_ENCRYPTION_KEY_ID]: encode(this.defaultKeyId),
},
// Encrypt entire payload, preserving metadata
data: await encrypt(
temporal.api.common.v1.Payload.encode(payload).finish(),
this.keys.get(this.defaultKeyId)!, // eslint-disable-line
@typescript-eslint/no-non-null-assertion
),
})),
);
}

async decode(payloads: Payload[]): Promise<Payload[]> {


return Promise.all(
payloads.map(async (payload) => {
if (
!payload.metadata
|| decode(payload.metadata[METADATA_ENCODING_KEY]) !==
ENCODING
) {
return payload;
}
if (!payload.data) {
throw new ValueError('Payload data is missing');
}

const keyIdBytes =
payload.metadata[METADATA_ENCRYPTION_KEY_ID];
if (!keyIdBytes) {
throw new ValueError(
'Unable to decrypt Payload without encryption key id',
);
}

const keyId = decode(keyIdBytes);


let key = this.keys.get(keyId);
if (!key) {
key = await fetchKey(keyId);
this.keys.set(keyId, key);
}
const decryptedPayloadBytes = await decrypt(payload.data, key);
console.log('Decrypting payload.data:', payload.data);
return
temporal.api.common.v1.Payload.decode(decryptedPayloadBytes);
}),
);

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 35/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

}
}

async function fetchKey(_keyId: string): Promise<crypto.CryptoKey> {


// In production, fetch key from a key management system (KMS). You
may want to memoize requests if you'll be decoding
// Payloads that were encrypted using keys other than defaultKeyId.
const key = Buffer.from('test-key-test-key-test-key-test!');
const cryptoKey = await crypto.subtle.importKey(
'raw',
key,
{
name: 'AES-GCM',
},
true,
['encrypt', 'decrypt'],
);

return cryptoKey;
}

The encryption and decryption code is in src/crypto.ts. Because encryption is CPU intensive,
and doing AES with the crypto module built into Node.js blocks the main thread, we use
@ronomon/crypto-async , which uses the Node.js thread pool.

As before, we provide a custom Data Converter to the Client and Worker:


encryption/src/client.ts
TypeScript JavaScript

const client = new Client({


dataConverter: await getDataConverter(),
});

const handle = await client.workflow.start(example, {


args: ['Alice: Private message for Bob.'],
taskQueue: 'encryption',
workflowId: `my-business-id-${uuid()}`,
});

console.log(`Started workflow ${handle.workflowId}`);


console.log(await handle.result());

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 36/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

encryption/src/worker.ts
TypeScript JavaScript

const worker = await Worker.create({


workflowsPath: require.resolve('./workflows'),
taskQueue: 'encryption',
dataConverter: await getDataConverter(),
});

When the Client sends 'Alice: Private message for Bob.' to the Workflow, it gets
encrypted on the Client and decrypted in the Worker. The Workflow receives the decrypted
message and appends another message. When it returns that longer string, the string gets
encrypted by the Worker and decrypted by the Client.
encryption/src/workflows.ts
TypeScript JavaScript

export async function example(message: string): Promise<string> {


return `${message}\nBob: Hi Alice, I'm Workflow Bob.`;
}

How to implement interceptors in TypeScript


Interceptors are a mechanism for modifying inbound and outbound SDK calls. Interceptors are
commonly used to add tracing and authorization to the scheduling and execution of Workflows
and Activities. You can compare these to "middleware" in other frameworks.
The TypeScript SDK comes with an optional interceptor package that adds tracing with
OpenTelemetry. See how to use it in the interceptors-opentelemetry code sample.
Interceptor types

WorkflowInboundCallsInterceptor: Intercept Workflow inbound calls like execution, Signals,


and Queries.

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 37/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

WorkflowOutboundCallsInterceptor: Intercept Workflow outbound calls to Temporal APIs


like scheduling Activities and starting Timers.
ActivityInboundCallsInterceptor: Intercept inbound calls to an Activity (such as execute ).
WorkflowClientCallsInterceptor: Intercept methods of WorkflowClient and
WorkflowHandle like starting or signaling a Workflow.

How interceptors work

Interceptors are run in a chain, and all interceptors work similarly. They accept two arguments:
input and next , where next calls the next interceptor in the chain. All interceptor methods
are optional—it's up to the implementor to choose which methods to intercept.
Interceptor examples

Log start and completion of Activities


TypeScript JavaScript

import {
ActivityInput,
Next,
WorkflowOutboundCallsInterceptor,
} from '@temporalio/workflow';

export class ActivityLogInterceptor


implements WorkflowOutboundCallsInterceptor
{
constructor(public readonly workflowType: string) {}

async scheduleActivity(
input: ActivityInput,
next: Next<WorkflowOutboundCallsInterceptor, 'scheduleActivity'>,
): Promise<unknown> {
console.log('Starting activity', { activityType: input.activityType
});
try {
return await next(input);
} finally {
console.log('Completed activity', {
workflow: this.workflowType,
activityType: input.activityType,
});
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 38/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

}
}
}

Authorization
TypeScript JavaScript

import {
defaultDataConverter,
Next,
WorkflowInboundCallsInterceptor,
WorkflowInput,
} from '@temporalio/workflow';

/**
* WARNING: This demo is meant as a simple auth example.
* Do not use this for actual authorization logic.
* Auth headers should be encrypted and credentials
* stored outside of the codebase.
*/
export class DumbWorkflowAuthInterceptor
implements WorkflowInboundCallsInterceptor
{
public async execute(
input: WorkflowInput,
next: Next<WorkflowInboundCallsInterceptor, 'execute'>,
): Promise<unknown> {
const authHeader = input.headers.auth;
const { user, password } = authHeader
? await defaultDataConverter.fromPayload(authHeader)
: undefined;

if (!(user === 'admin' && password === 'admin')) {


throw new Error('Unauthorized');
}
return await next(input);
}
}

To properly do authorization from Workflow code, the Workflow would need to access
encryption keys and possibly authenticate against an external user database, which requires
the Workflow to break isolation. Please contact us if you need to discuss this further.
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 39/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

Interceptor registration

Activity and client interceptors registration


Activity interceptors are registered on Worker creation by passing an array of
ActivityInboundCallsInterceptor factory functions through WorkerOptions.
Client interceptors are registered on WorkflowClient construction by passing an array
of WorkflowClientCallsInterceptor factory functions via WorkflowClientOptions.
Workflow interceptors registration
Workflow interceptor registration is different from the other interceptors because they run in
the Workflow isolate. To register Workflow interceptors, export an interceptors function
from a file located in the workflows directory and provide the name of that file to the Worker
on creation via WorkerOptions.
At the time of construction, the Workflow context is already initialized for the current Workflow.
Use workflowInfo to add Workflow-specific information in the interceptor.
src/workflows/your-interceptors.ts

TypeScript JavaScript

import { workflowInfo } from '@temporalio/workflow';

export const interceptors = () => ({


outbound: [new ActivityLogInterceptor(workflowInfo().workflowType)],
inbound: [],
});

src/worker/index.ts

TypeScript JavaScript

const worker = await Worker.create({


workflowsPath: require.resolve('./workflows'),
interceptors: {
workflowModules: [require.resolve('./workflows/your-
interceptors')],
https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 40/41
02/08/2023, 06:53 TypeScript SDK developer's guide - Features | Temporal Documentation

},
});

Tags: guide-context developer-guide sdk typescript how-to timers sleep

https://docs.temporal.io/dev-guide/typescript/features#asynchronous-design-patterns 41/41

You might also like