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.
This guide is a work in progress. Some sections may be incomplete or missing for some languages. Information may change at any time.
If you can't find what you are looking for in the Developer's guide, it could be in older docs for SDKs.
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
Signals
A SignalWhat is a Signal?
A Signal is an asynchronous request to a Workflow Execution.
Learn more 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.
Define Signal
A Signal has a name and can have arguments.
- The name, also called a Signal type, is a string.
- The arguments must be serializableWhat is a Data Converter?
A Data Converter is a Temporal SDK component that serializes and encodes data entering and exiting a Temporal Cluster.
Learn more.
- TypeScript
- JavaScript
import { defineSignal } from '@temporalio/workflow';
interface JoinInput {
userId: string;
groupId: string;
}
export const joinSignal = defineSignal<[JoinInput]>('join');
import { defineSignal } from '@temporalio/workflow';
export const joinSignal = defineSignal('join');
Handle Signal
Workflows listen for Signals by the Signal's name.
- 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]));
}
});
}
import { setHandler } from '@temporalio/workflow';
export async function yourWorkflow() {
const groups = new Map();
setHandler(joinSignal, ({ userId, groupId }) => {
const group = groups.get(groupId);
if (group) {
group.add(userId);
}
else {
groups.set(groupId, new Set([userId]));
}
});
}
Send Signal from Client
When a Signal is sent successfully from the Temporal Client, the WorkflowExecutionSignaledEvents reference
Events are created by the Temporal Cluster in response to external occurrences and Commands generated by a Workflow Execution.
Learn more Event appears in the Event History of the Workflow that receives the Signal.
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' });
Send Signal from 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 SignalExternalWorkflowExecutionInitiatedEvents reference
Events are created by the Temporal Cluster in response to external occurrences and Commands generated by a Workflow Execution.
Learn more Event appears in the sender's Event History. - A WorkflowExecutionSignaledEvents reference
Events are created by the Temporal Cluster in response to external occurrences and Commands generated by a Workflow Execution.
Learn more Event appears in the recipient's Event History.
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' });
}
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
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' }],
});
Queries
A QueryWhat is a Query?
A Query is a synchronous operation that is used to report the state of a Workflow Execution.
Learn more is a synchronous operation that is used to get the state of a Workflow Execution.
Define Query
A Query has a name and can have arguments.
- The name, also called a Query type, is a string.
- The arguments must be serializableWhat is a Data Converter?
A Data Converter is a Temporal SDK component that serializes and encodes data entering and exiting a Temporal Cluster.
Learn more.
Use defineQuery
to define the name, parameters, and return value of a Query.
- TypeScript
- JavaScript
import { defineQuery } from '@temporalio/workflow';
export const getValueQuery = defineQuery<number | undefined, [string]>(
'getValue',
);
import { defineQuery } from '@temporalio/workflow';
export const getValueQuery = defineQuery('getValue');
Handle Query
Queries are handled by your Workflow.
Don’t include any logic that causes CommandWhat is a Command?
A Command is a requested action issued by a Worker to the Temporal Cluster after a Workflow Task Execution completes.
Learn more 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.
- 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;
}
export async function trackState() {
const state = new Map();
setHandler(setValueSignal, (key, value) => void state.set(key, value));
setHandler(getValueQuery, (key) => state.get(key));
await CancellationScope.current().cancelRequested;
}
Send Query
Queries are sent from a Temporal Client.
Use WorkflowHandle.query
to query a running or completed Workflow.
- 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 });
}
import { Client } from '@temporalio/client';
import { getValueQuery } from './workflows';
async function run() {
const client = new Client();
const handle = client.workflow.getHandle('state-id-0');
const meaning = await handle.query(getValueQuery, 'meaning-of-life');
console.log({ meaning });
}
Static and dynamic Signals and Queries
- 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;
}
}
import * as wf from '@temporalio/workflow';
export const unblockSignal = wf.defineSignal('unblock');
export const isBlockedQuery = wf.defineQuery('isBlocked');
export async function unblockOrCancel() {
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
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 */
});
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
Why not "new Signal" and "new Query"?
The semantic of defineSignal
and defineQuery
is intentional.
They return Signal and Query definitions, not unique instances of Signals and Queries themselves
The following is their entire source code:
- TypeScript
- JavaScript
/**
* Define a signal method for a Workflow.
*/
export function defineSignal<Args extends any[] = []>(
name: string,
): SignalDefinition<Args> {
return {
type: 'signal',
name,
};
}
/**
* Define a query method for a Workflow.
*/
export function defineQuery<Ret, Args extends any[] = []>(
name: string,
): QueryDefinition<Ret, Args> {
return {
type: 'query',
name,
};
}
/**
* Define a signal method for a Workflow.
*/
export function defineSignal(name) {
return {
type: 'signal',
name,
};
}
/**
* Define a query method for a Workflow.
*/
export function defineQuery(name) {
return {
type: 'query',
name,
};
}
Signals and Queries are instantiated only in setHandler
and are specific to particular Workflow Executions.
These distinctions might seem minor, but they model how Temporal works under the hood, because Signals and Queries are messages identified by "just strings" and don't have meaning independent of the Workflow having a listener to handle them. This will be clearer if you refer to the Client-side APIs.
Why setHandler and not OTHER_API?
We named it setHandler
instead of subscribe
because a Signal or Query can have only one "handler" at a time, whereas subscribe
could imply an Observable with multiple consumers and is a higher-level construct.
- TypeScript
- JavaScript
wf.setHandler(MySignal, handlerFn1);
wf.setHandler(MySignal, handlerFn2); // replaces handlerFn1
wf.setHandler(MySignal, handlerFn1);
wf.setHandler(MySignal, handlerFn2); // replaces handlerFn1
If you are familiar with RxJS, you are free to wrap your Signals and Queries into Observables if you want, or you could dynamically reassign the listener based on your business logic or Workflow state.
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 ExecutionWorkflow timeouts
Each Workflow timeout controls the maximum duration of a different aspect of a Workflow Execution.
Learn more.
- Workflow Execution TimeoutWhat is a Workflow Execution Timeout?
A Workflow Execution Timeout is the maximum time that a Workflow Execution can be executing (have an Open status) including retries and any usage of Continue As New.
Learn more - restricts the maximum amount of time that a single Workflow Execution can be executed. - Workflow Run TimeoutWhat is a Workflow Run Timeout?
This is the maximum amount of time that a single Workflow Run is restricted to.
Learn more: restricts the maximum amount of time that a single Workflow Run can last. - Workflow Task TimeoutWhat is a Workflow Task Timeout?
A Workflow Task Timeout is the maximum amount of time that the Temporal Server will wait for a Worker to start processing a Workflow Task after the Task has been pulled from the Task Queue.
Learn more: 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:
- TypeScript
- JavaScript
await client.workflow.start(example, {
taskQueue,
workflowId,
workflowExecutionTimeout: '1 day',
});
await client.workflow.start(example, {
taskQueue,
workflowId,
workflowExecutionTimeout: '1 day',
});
- TypeScript
- JavaScript
await client.workflow.start(example, {
taskQueue,
workflowId,
workflowRunTimeout: '1 minute',
});
await client.workflow.start(example, {
taskQueue,
workflowId,
workflowRunTimeout: '1 minute',
});
- TypeScript
- JavaScript
await client.workflow.start(example, {
taskQueue,
workflowId,
workflowTaskTimeout: '1 minute',
});
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 PolicyWhat is a Retry Policy?
A Retry Policy is a collection of attributes that instructs the Temporal Server how to retry a failure of a Workflow Execution or an Activity Task Execution.
Learn more 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.
- TypeScript
- JavaScript
const handle = await client.workflow.start(example, {
taskQueue,
workflowId,
retry: {
maximumAttempts: 3,
},
});
const handle = await client.workflow.start(example, {
taskQueue,
workflowId,
retry: {
maximumAttempts: 3,
},
});
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 TimeoutWhat is a Schedule-To-Close Timeout?
A Schedule-To-Close Timeout is the maximum amount of time allowed for the overall Activity Execution, from when the first Activity Task is scheduled to when the last Activity Task, in the chain of Activity Tasks that make up the Activity Execution, reaches a Closed status.
Learn more: is the maximum amount of time allowed for the overall Activity ExecutionWhat is an Activity Execution?
An Activity Execution is the full chain of Activity Task Executions.
Learn more. - Start-To-Close TimeoutWhat is a Start-To-Close Timeout?
A Start-To-Close Timeout is the maximum time allowed for a single Activity Task Execution.
Learn more: is the maximum time allowed for a single Activity Task ExecutionWhat is an Activity Task Execution?
An Activity Task Execution occurs when a Worker uses the context provided from the Activity Task and executes the Activity Definition.
Learn more. - Schedule-To-Start TimeoutWhat is a Schedule-To-Start Timeout?
A Schedule-To-Start Timeout is the maximum amount of time that is allowed from when an Activity Task is placed in a Task Queue to when a Worker picks it up from the Task Queue.
Learn more: is the maximum amount of time that is allowed from when an Activity TaskWhat is an Activity Task?
An Activity Task contains the context needed to make an Activity Task Execution.
Learn more is scheduled to when a WorkerWhat is a Worker?
In day-to-day conversations, the term Worker is used to denote both a Worker Program and a Worker Process. Temporal documentation aims to be explicit and differentiate between them.
Learn more 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:
// 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: [],
},
});
Activity retries
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 PolicyWhat is a Retry Policy?
A Retry Policy is a collection of attributes that instructs the Temporal Server how to retry a failure of a Workflow Execution or an Activity Task Execution.
Learn more 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: [],
},
});
Activity retry simulator
Use this tool to visualize total Activity Execution times and experiment with different Activity timeouts and Retry Policies.
The simulator is based on a common Activity use-case, which is to call a third party HTTP API and return the results. See the example code snippets below.
Use the Activity Retries settings to configure how long the API request takes to succeed or fail. There is an option to generate scenarios. The Task Time in Queue simulates the time the Activity Task might be waiting in the Task Queue.
Use the Activity Timeouts and Retry Policy settings to see how they impact the success or failure of an Activity Execution.
Sample Activity
import axios from 'axios';
async function testActivity(url: string): Promise<void> {
await axios.get(url);
}
export default testActivity;
Activity Retries (in ms)
Activity Timeouts (in ms)
Retry Policy (in ms)
Success after 1 ms
{
"startToCloseTimeout": 10000,
"retryPolicy": {
"backoffCoefficient": 2,
"initialInterval": 1000
}
}
Activity Heartbeats
An Activity HeartbeatWhat is an Activity Heartbeat?
An Activity Heartbeat is a ping from the Worker that is executing the Activity to the Temporal Cluster. Each ping informs the Temporal Cluster that the Activity Execution is making progress and the Worker has not crashed.
Learn more is a ping from the Worker ProcessWhat is a Worker Process?
A Worker Process is responsible for polling a Task Queue, dequeueing a Task, executing your code in response to a Task, and responding to the Temporal Server with the results.
Learn more that is executing the Activity to the Temporal ClusterWhat is a Temporal Cluster?
A Temporal Cluster is a Temporal Server paired with Persistence and Visibility stores.
Learn more.
Each Heartbeat informs the Temporal Cluster that the Activity ExecutionWhat is an Activity Execution?
An Activity Execution is the full chain of Activity Task Executions.
Learn more is making progress and the Worker has not crashed.
If the Cluster does not receive a Heartbeat within a Heartbeat TimeoutWhat is a Heartbeat Timeout?
A Heartbeat Timeout is the maximum time between Activity Heartbeats.
Learn more time period, the Activity will be considered failed and another Activity Task ExecutionWhat is an Activity Task Execution?
An Activity Task Execution occurs when a Worker uses the context provided from the Activity Task and executes the Activity Definition.
Learn more may be scheduled according to the Retry Policy.
Heartbeats may not always be sent to the Cluster—they may be throttledWhat is an Activity Heartbeat?
An Activity Heartbeat is a ping from the Worker that is executing the Activity to the Temporal Cluster. Each ping informs the Temporal Cluster that the Activity Execution is making progress and the Worker has not crashed.
Learn more 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.
Long-running Activities should Heartbeat their progress back to the Workflow for earlier detection of stalled Activities (with Heartbeat TimeoutWhat is a Heartbeat Timeout?
A Heartbeat Timeout is the maximum time between Activity Heartbeats.
Learn more) 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',
});
// activity implementation
export async function example(sleepIntervalMs = 1000) {
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({
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
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);
}
}
export async function example(sleepIntervalMs = 1000) {
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.
Heartbeat Timeout
A Heartbeat TimeoutWhat is a Heartbeat Timeout?
A Heartbeat Timeout is the maximum time between Activity Heartbeats.
Learn more works in conjunction with Activity HeartbeatsWhat is an Activity Heartbeat?
An Activity Heartbeat is a ping from the Worker that is executing the Activity to the Temporal Cluster. Each ping informs the Temporal Cluster that the Activity Execution is making progress and the Worker has not crashed.
Learn more.
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,
});
Asynchronous Activity Completion
Asynchronous Activity CompletionWhat is Asynchronous Activity Completion?Asynchronous Activity Completion occurs when an external system provides the final result of a computation, started by an Activity, to the Temporal System.
Learn more enables the Activity Function to return without the Activity Execution completing.
There are three steps to follow:
- The Activity provides the external system with identifying information needed to complete the Activity Execution.
Identifying information can be a Task TokenWhat is a Task Token?
A Task Token is a unique Id that correlates to an Activity Execution.
Learn more, or a combination of Namespace, Workflow Id, and Activity Id. - The Activity Function completes in a way that identifies it as waiting to be completed by an external system.
- 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!');
}
import { CompleteAsyncError, Context } from '@temporalio/activity';
import { AsyncCompletionClient } from '@temporalio/client';
export async function doSomethingAsync() {
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) {
const client = new AsyncCompletionClient();
// does some work...
await client.complete(taskToken, 'Job\'s done!');
}
Local Activities
To call Local ActivitiesWhat is a Local Activity?
A Local Activity is an Activity Execution that executes in the same process as the Workflow Execution that spawns it.
Learn more 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');
// ...
}
import * as workflow from '@temporalio/workflow';
const { getEnvVar } = workflow.proxyLocalActivities({
startToCloseTimeout: '2 seconds',
});
export async function yourWorkflow() {
const someSetting = await getEnvVar('SOME_SETTING');
// ...
}
Local Activities must be registered with the Worker the same way non-local Activities are.
Cancel an Activity
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 TimeoutWhat is a Heartbeat Timeout?
A Heartbeat Timeout is the maximum time between Activity Heartbeats.
Learn more 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.
Unlike regular Activities, Local ActivitiesWhat is a Local Activity?
A Local Activity is an Activity Execution that executes in the same process as the Workflow Execution that spawns it.
Learn more 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.
Child Workflows
A Child Workflow ExecutionWhat is a Child Workflow Execution?
A Child Workflow Execution is a Workflow Execution that is spawned from within another Workflow.
Learn more 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 (StartChildWorkflowExecutionInitiatedEvents reference
Events are created by the Temporal Cluster in response to external occurrences and Commands generated by a Workflow Execution.
Learn more, ChildWorkflowExecutionStartedEvents reference
Events are created by the Temporal Cluster in response to external occurrences and Commands generated by a Workflow Execution.
Learn more, ChildWorkflowExecutionCompletedEvents reference
Events are created by the Temporal Cluster in response to external occurrences and Commands generated by a Workflow Execution.
Learn more, etc...) are logged in the Workflow Execution Event History.
Always block progress until the ChildWorkflowExecutionStartedEvents reference
Events are created by the Temporal Cluster in response to external occurrences and Commands generated by a Workflow Execution.
Learn more Event is logged to the Event History to ensure the Child Workflow Execution has started.
After that, Child Workflow Executions may be abandoned using the default Abandon Parent Close PolicyWhat is a Parent Close Policy?
If a Workflow Execution is a Child Workflow Execution, a Parent Close Policy determines what happens to the Workflow Execution if its Parent Workflow Execution changes to a Closed status (Completed, Failed, Timed out).
Learn more set in the Child Workflow Options.
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 and return a handle to it, use startChild
.
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[]): 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');
}
import { executeChild } from '@temporalio/workflow';
export async function parentWorkflow(...names) {
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');
}
Parent Close Policy
A Parent Close PolicyWhat is a Parent Close Policy?
If a Workflow Execution is a Child Workflow Execution, a Parent Close Policy determines what happens to the Workflow Execution if its Parent Workflow Execution changes to a Closed status (Completed, Failed, Timed out).
Learn more 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');
}
import { executeChild } from '@temporalio/workflow';
export async function parentWorkflow(...names) {
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');
}
Continue-As-New
Continue-As-NewWhat is Continue-As-New?Continue-As-New is the mechanism by which all relevant state is passed to a new Workflow Execution with a fresh Event History.
Learn more 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 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-NewWhat is Continue-As-New?
Continue-As-New is the mechanism by which all relevant state is passed to a new Workflow Execution with a fresh Event History.
Learn more, 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.
}
import { continueAsNew, sleep } from '@temporalio/workflow';
export async function loopingWorkflow(iteration = 0) {
if (iteration === 10) {
return;
}
console.log('Running Workflow iteration:', iteration);
await sleep(1000);
// Must match the arguments expected by `loopingWorkflow`
await continueAsNew(iteration + 1);
// Unreachable code, continueAsNew is like `process.exit` and will stop execution once called.
}
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.
Create
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.
Backfill
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.
Delete
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.
Describe
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.
List
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.
Pause
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.
Trigger
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.
Update
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.
Timers
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
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
Use Promise.race
with Timers to dynamically adjust delays.
- TypeScript
- JavaScript
export async function processOrderWorkflow({
orderProcessingMS,
sendDelayedEmailTimeoutMS,
}: ProcessOrderOptions): Promise<void> {
let processing = true;
const processOrderPromise = processOrder(orderProcessingMS).then(() => {
processing = false;
});
await Promise.race([processOrderPromise, sleep(sendDelayedEmailTimeoutMS)]);
if (processing) {
await sendNotificationEmail();
await processOrderPromise;
}
}
export async function processOrderWorkflow({ orderProcessingMS, sendDelayedEmailTimeoutMS, }) {
let processing = true;
const processOrderPromise = processOrder(orderProcessingMS).then(() => {
processing = false;
});
await Promise.race([processOrderPromise, sleep(sendDelayedEmailTimeoutMS)]);
if (processing) {
await sendNotificationEmail();
await processOrderPromise;
}
}
Racing Signals
Use Promise.race
with Signals and Triggers to have a promise resolve at the earlier of either system time or human intervention.
- TypeScript
- JavaScript
import { defineSignal, sleep, Trigger } from '@temporalio/workflow';
const userInteraction = new Trigger<boolean>();
const completeUserInteraction = defineSignal('completeUserInteraction');
export async function yourWorkflow(userId: string) {
setHandler(completeUserInteraction, () => userInteraction.resolve(true)); // programmatic resolve
const userInteracted = await Promise.race([
userInteraction,
sleep('30 days'),
]);
if (!userInteracted) {
await sendReminderEmail(userId);
}
}
import { defineSignal, sleep, Trigger } from '@temporalio/workflow';
const userInteraction = new Trigger();
const completeUserInteraction = defineSignal('completeUserInteraction');
export async function yourWorkflow(userId) {
setHandler(completeUserInteraction, () => userInteraction.resolve(true)); // programmatic resolve
const userInteracted = await Promise.race([
userInteraction,
sleep('30 days'),
]);
if (!userInteracted) {
await sendReminderEmail(userId);
}
}
You can invert this to create a reminder pattern where the promise resolves if no Signal is received.
Be careful when racing a chained sleep
.
This might cause bugs because the chained .then
will still continue to execute.
await Promise.race([
sleep('5s').then(() => (status = 'timed_out')),
somethingElse.then(() => (status = 'processed')),
]);
if (status === 'processed') await complete(); // takes more than 5 seconds
// status = timed_out
Updatable Timer
Here is how you can build an updatable Timer with condition
:
- TypeScript
- JavaScript
import * as wf from '@temporalio/workflow';
// usage
export async function countdownWorkflow(): Promise<void> {
const target = Date.now() + 24 * 60 * 60 * 1000; // 1 day!!!
const timer = new UpdatableTimer(target);
console.log('timer set for: ' + new Date(target).toString());
wf.setHandler(setDeadlineSignal, (deadline) => {
// send in new deadlines via Signal
timer.deadline = deadline;
console.log('timer now set for: ' + new Date(deadline).toString());
});
wf.setHandler(timeLeftQuery, () => timer.deadline - Date.now());
await timer; // if you send in a signal with a new time, this timer will resolve earlier!
console.log('countdown done!');
}
import * as wf from '@temporalio/workflow';
// usage
export async function countdownWorkflow() {
const target = Date.now() + 24 * 60 * 60 * 1000; // 1 day!!!
const timer = new UpdatableTimer(target);
console.log('timer set for: ' + new Date(target).toString());
wf.setHandler(setDeadlineSignal, (deadline) => {
// send in new deadlines via Signal
timer.deadline = deadline;
console.log('timer now set for: ' + new Date(deadline).toString());
});
wf.setHandler(timeLeftQuery, () => timer.deadline - Date.now());
await timer; // if you send in a signal with a new time, this timer will resolve earlier!
console.log('countdown done!');
}
This is available in the third-party package temporal-time-utils
, where you can also see the implementation:
- TypeScript
- JavaScript
// implementation
export class UpdatableTimer implements PromiseLike<void> {
deadlineUpdated = false;
#deadline: number;
constructor(deadline: number) {
this.#deadline = deadline;
}
private async run(): Promise<void> {
/* eslint-disable no-constant-condition */
while (true) {
this.deadlineUpdated = false;
if (
!(await wf.condition(
() => this.deadlineUpdated,
this.#deadline - Date.now(),
))
) {
break;
}
}
}
then<TResult1 = void, TResult2 = never>(
onfulfilled?: (value: void) => TResult1 | PromiseLike<TResult1>,
onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>,
): PromiseLike<TResult1 | TResult2> {
return this.run().then(onfulfilled, onrejected);
}
set deadline(value: number) {
this.#deadline = value;
this.deadlineUpdated = true;
}
get deadline(): number {
return this.#deadline;
}
}
// implementation
export class UpdatableTimer {
deadlineUpdated = false;
#deadline;
constructor(deadline) {
this.#deadline = deadline;
}
async run() {
/* eslint-disable no-constant-condition */
while (true) {
this.deadlineUpdated = false;
if (!(await wf.condition(() => this.deadlineUpdated, this.#deadline - Date.now()))) {
break;
}
}
}
then(onfulfilled, onrejected) {
return this.run().then(onfulfilled, onrejected);
}
set deadline(value) {
this.#deadline = value;
this.deadlineUpdated = true;
}
get deadline() {
return this.#deadline;
}
}
Temporal Cron Jobs
A Temporal Cron JobWhat is a Temporal Cron Job?
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.
Learn more 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.
You can set each Workflow to repeat on a schedule with the cronSchedule
option:
const handle = await client.start(scheduledWorkflow, {
// ...
cronSchedule: '* * * * *', // start every minute
});
Namespaces
You can create, update, deprecate or delete your NamespacesWhat is a Namespace?
A Namespace is a unit of isolation within the Temporal Platform
Learn more 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 UIHow to create a Namespace in Temporal Cloud
To create a Namespace in Temporal Cloud, use either Temporal Cloud UI or tcld.
Learn more 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 AuthorizerWhat is an Authorizer Plugin?
undefined
Learn more 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.
Register Namespace
Registering a Namespace creates a Namespace on the Temporal Cluster or Temporal Cloud.
On Temporal Cloud, use the Temporal Cloud UIHow to create a Namespace in Temporal Cloud
To create a Namespace in Temporal Cloud, use either Temporal Cloud UI or tcld.
Learn more 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.
Use a custom AuthorizerWhat is an Authorizer Plugin?
undefined
Learn more on your Frontend Service in the Temporal Cluster to set restrictions on who can create, update, or deprecate Namespaces.
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 UIHow to create a Namespace in Temporal Cloud
To create a Namespace in Temporal Cloud, use either Temporal Cloud UI or tcld.
Learn more 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 AuthorizerWhat is an Authorizer Plugin?
undefined
Learn more 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.
Custom payload conversion
Temporal SDKs provide a Payload ConverterWhat is a Payload Converter?
A Payload Converter serializes data, converting objects or values to bytes and back.
Learn more that can be customized to convert a custom data type to PayloadWhat is a Payload?
A Payload represents binary data such as input and output from Activities and Workflows.
Learn more and back.
Implementing custom Payload conversion is optional.
It is needed only if the default Data ConverterWhat is a default Data Converter?
The default Data Converter is used by the Temporal SDK to convert objects into bytes using a series of Payload Converters.
Learn more does not support your custom values.
To support custom Payload conversion, create a custom Payload ConverterWhat is a Payload Converter?
A Payload Converter serializes data, converting objects or values to bytes and back.
Learn more 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.
Interceptors
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.
- 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
andWorkflowHandle
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,
});
}
}
}
export class ActivityLogInterceptor {
workflowType;
constructor(workflowType) {
this.workflowType = workflowType;
}
async scheduleActivity(input, next) {
console.log('Starting activity', { activityType: input.activityType });
try {
return await next(input);
}
finally {
console.log('Completed activity', {
workflow: this.workflowType,
activityType: input.activityType,
});
}
}
}
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);
}
}
import { defaultDataConverter, } 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 {
async execute(input, next) {
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.
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: [],
});
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')],
},
});
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
interceptors: {
workflowModules: [require.resolve('./workflows/your-interceptors')],
},
});