TypeScript SDK developer's guide - Foundations
The Foundations section of the Temporal Developer's guide covers the minimum set of concepts and implementation details needed to build and run a Temporal ApplicationWhat is a Temporal Application
A Temporal Application is a set of Workflow Executions.
Learn more—that is, all the relevant steps to start a Workflow Execution that executes an Activity.
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:
- Run a development ClusterHow to install Temporal CLI and run a development server
undefined
Learn more - Install your SDKHow to install a Temporal SDK
A Temporal SDK provides a framework for Temporal Application development.
Learn more - Connect to a dev ClusterHow to connect a Temporal Client to a Temporal Cluster
When connecting a Temporal Client to a Temporal Cluster, you must provide the address and port number of the Temporal Cluster.
Learn more - Connect to Temporal CloudHow to connect to Temporal Cloud
Use a compatible mTLS CA certificate and mTLS private key and your Cloud Namespace to connect to Temporal Cloud.
Learn more - Develop a WorkflowHow to develop a basic Workflow
Workflows are the fundamental unit of a Temporal Application, and it all starts with the development of a Workflow Definition.
Learn more - Develop an ActivityHow to develop a basic Activity
One of the primary things that Workflows do is orchestrate the execution of Activities.
Learn more - Start an Activity ExecutionHow to start an Activity Execution
Calls to spawn Activity Executions are written within a Workflow Definition.
Learn more - Run a dev WorkerHow to run Worker Processes
The Worker Process is where Workflow Functions and Activity Functions are executed.
Learn more - Run a Worker on DockerHow to run a Worker on Docker in TypeScript
Workers based on the TypeScript SDK can be deployed and run as Docker containers.
Learn more - Run a Temporal Cloud WorkerHow to run Worker Processes
The Worker Process is where Workflow Functions and Activity Functions are executed.
Learn more - Start a Workflow ExecutionHow to start a Workflow Execution
Workflow Execution semantics rely on several parameters—that is, to start a Workflow Execution you must supply a Task Queue that will be used for the Tasks (one that a Worker is polling), the Workflow Type, language-specific contextual data, and Workflow Function parameters.
Learn more
Run a development server
This section describes how to install the Temporal CLI and run a development Cluster. The local development Cluster comes packaged with the Temporal Web UI.
For information on deploying and running a production Cluster, see the Cluster deployment guide, or sign up for Temporal Cloud and let us run your production Cluster for you.
Temporal CLI is a tool for interacting with a Temporal Cluster from the command line and it includes a distribution of the Temporal Server and Web UI. This local development Cluster runs as a single process with zero runtime dependencies and it supports persistence to disk and in-memory mode through SQLite.
Install the Temporal CLI
Choose one of the following install methods to install the Temporal CLI.
- macOS
- Linux
- Windows
Install the Temporal CLI with Homebrew.
brew install temporal
Install the Temporal CLI with cURL.
curl -sSf https://temporal.download/cli.sh | sh
Install the Temporal CLI from CDN.
- Select the platform and architecture needed.
- Extract the downloaded archive.
- Add the
temporal
binary to your PATH.
Install the Temporal CLI with cURL.
curl -sSf https://temporal.download/cli.sh | sh
Install the Temporal CLI from CDN.
- Select the platform and architecture needed.
- Extract the downloaded archive.
- Add the
temporal
binary to your PATH.
- Install the Temporal CLI from CDN.
- Select the platform and architecture needed and download the binary.
- Extract the downloaded archive.
- Add the
temporal.exe
binary to your PATH.
Start the Temporal Development Server
Start the Temporal Development Server by using the server start-dev
command.
temporal server start-dev
This command automatically starts the Web UI, creates the default Namespace, and uses an in-memory database.
The Temporal Server should be available on localhost:7233
and the Temporal Web UI should be accessible at http://localhost:8233
.
The server's startup configuration can be customized using command line options. For a full list of options, run:
temporal server start-dev --help
Install a Temporal SDK
A Temporal SDKWhat is a Temporal SDK?
A Temporal SDK is a language-specific library that offers APIs to construct and use a Temporal Client to communicate with a Temporal Cluster, develop Workflow Definitions, and develop Worker Programs.
Learn more provides a framework for Temporal ApplicationWhat is a Temporal Application
A Temporal Application is a set of Workflow Executions.
Learn more development.
An SDK provides you with the following:
- A Temporal ClientWhat is a Temporal Client
A Temporal Client, provided by a Temporal SDK, provides a set of APIs to communicate with a Temporal Cluster.
Learn more to communicate with a Temporal ClusterWhat is a Temporal Cluster?
A Temporal Cluster is a Temporal Server paired with Persistence and Visibility stores.
Learn more. - APIs to develop WorkflowsWhat is a Workflow?
In day-to-day conversations, the term "Workflow" frequently denotes either a Workflow Type, a Workflow Definition, or a Workflow Execution.
Learn more. - APIs to create and manage Worker ProcessesWhat 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. - APIs to author ActivitiesWhat is an Activity Definition?
An Activity Definition is the code that defines the constraints of an Activity Task Execution.
Learn more.
This project requires Node.js 14.18 or later.
Create a project
npx @temporalio/create@latest ./your-app
Add to an existing project
npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity @temporalio/common
The TypeScript SDK is designed with TypeScript-first developer experience in mind, but it works equally well with JavaScript.
API reference
The Temporal TypeScript SDK API reference is published to typescript.temporal.io.
Code samples
You can find a complete list of executable code samples in Temporal's GitHub repository.
Additionally, several of the Tutorials are backed by a fully executable template application.
Use the TypeScript samples library stored on GitHub to demonstrate various capabilities of Temporal.
Where can I find video demos?
Temporal TypeScript YouTube playlist.
Linting and types
If you started your project with @temporalio/create
, you already have our recommended TypeScript and ESLint configurations.
If you incrementally added Temporal to an existing app, we do recommend setting up linting and types because they help catch bugs well before you ship them to production, and they improve your development feedback loop. Take a look at our recommended .eslintrc file and tweak to suit your needs.
Connect to a dev Cluster
A Temporal ClientWhat is a Temporal Client
A Temporal Client, provided by a Temporal SDK, provides a set of APIs to communicate with a Temporal Cluster.
Learn more enables you to communicate with the Temporal ClusterWhat is a Temporal Cluster?
A Temporal Cluster is a Temporal Server paired with Persistence and Visibility stores.
Learn more.
Communication with a Temporal Cluster includes, but isn't limited to, the following:
- Starting Workflow Executions.
- Sending Signals to Workflow Executions.
- Sending Queries to Workflow Executions.
- Getting the results of a Workflow Execution.
- Providing an Activity Task Token.
A Temporal Client cannot be initialized and used inside a Workflow. However, it is acceptable and common to use a Temporal Client inside an Activity to communicate with a Temporal Cluster.
When you are running a Cluster locally (such as Temporalite), the number of connection options you must provide is minimal.
Many SDKs default to the local host or IP address and port that Temporalite and Docker Compose serve (127.0.0.1:7233
).
Creating a Connection connects to the Temporal Cluster, and you can pass the Connection
instance when creating the Client.
If you omit the Connection
and just create a new Client()
, it will connect to localhost:7233
.
- TypeScript
- JavaScript
import { Client } from '@temporalio/client';
async function run() {
const client = new Client();
// . . .
await client.connection.close();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
import { Client } from '@temporalio/client';
async function run() {
const client = new Client();
// . . .
await client.connection.close();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
Connect to Temporal Cloud
When you connect to Temporal Cloud, you need to provide additional connection and client options that include the following:
- An address that includes your Cloud Namespace NameWhat is a Namespace?
A Namespace is a unit of isolation within the Temporal Platform
Learn more and a port number:<Namespace>.<ID>.tmprl.cloud:<port>
. - mTLS CA certificate.
- mTLS private key.
For more information about managing and generating client certificates for Temporal Cloud, see How to manage certificates in Temporal Cloud.
For more information about configuring TLS to secure inter- and intra-network communication for a Temporal Cluster, see Temporal Customization Samples.
Create a Connection
with a connectionOptions
object that has your Cloud namespace and client certificate.
- TypeScript
- JavaScript
import { Client, Connection } from '@temporalio/client';
import fs from 'fs-extra';
const { NODE_ENV = 'development' } = process.env;
const isDeployed = ['production', 'staging'].includes(NODE_ENV);
async function run() {
const cert = await fs.readFile('./path-to/your.pem');
const key = await fs.readFile('./path-to/your.key');
let connectionOptions = {};
if (isDeployed) {
connectionOptions = {
address: 'your-namespace.tmprl.cloud:7233',
tls: {
clientCertPair: {
crt: cert,
key,
},
},
};
const connection = await Connection.connect(connectionOptions);
const client = new Client({
connection,
namespace: 'your-namespace',
});
// . . .
await client.connection.close();
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
import { Client, Connection } from '@temporalio/client';
import fs from 'fs-extra';
const { NODE_ENV = 'development' } = process.env;
const isDeployed = ['production', 'staging'].includes(NODE_ENV);
async function run() {
const cert = await fs.readFile('./path-to/your.pem');
const key = await fs.readFile('./path-to/your.key');
let connectionOptions = {};
if (isDeployed) {
connectionOptions = {
address: 'your-namespace.tmprl.cloud:7233',
tls: {
clientCertPair: {
crt: cert,
key,
},
},
};
const connection = await Connection.connect(connectionOptions);
const client = new Client({
connection,
namespace: 'your-namespace',
});
// . . .
await client.connection.close();
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
Develop Workflows
Workflows are the fundamental unit of a Temporal Application, and it all starts with the development of a Workflow DefinitionWhat is a Workflow Definition?
A Workflow Definition is the code that defines the constraints of a Workflow Execution.
Learn more.
In the Temporal TypeScript SDK programming model, Workflow Definitions are just functions, which can store state and orchestrate Activity Functions.
The following code snippet uses proxyActivities
to schedule a greet
Activity in the system to say hello.
A Workflow Definition can have multiple parameters; however, we recommend using a single object parameter.
type ExampleArgs = {
name: string;
};
export async function example(
args: ExampleArgs,
): Promise<{ greeting: string }> {
const greeting = await greet(args.name);
return { greeting };
}
Workflow parameters
Temporal Workflows may have any number of custom parameters. However, we strongly recommend that objects are used as parameters, so that the object's individual fields may be altered without breaking the signature of the Workflow. All Workflow Definition parameters must be serializable.
You can define and pass parameters in your Workflow. In this example, you define your arguments in your client.ts
file and pass those parameters to workflow.ts
through your Workflow function.
Start a Workflow with the parameters that are in the client.ts
file. In this example we set the name
parameter to Temporal
and born
to 2019
. Then set the Task Queue and Workflow Id.
client.ts
import { example } from './workflows';
...
await client.start(example, {
args: [{ name: 'Temporal', born: 2019 }],
taskQueue: 'your-queue',
workflowId: 'business-meaningful-id',
});
In workflows.ts
define the type of the parameter that the Workflow function takes in. The interface ExampleParam
is a name we can now use to describe the requirement in the previous example. It still represents having the two properties called name
and born
that is of the type string
. Then define a function that takes in a parameter of the type ExampleParam
and return a Promise<string>
. The Promise
object represents the eventual completion, or failure, of await client.start()
and its resulting value.
- TypeScript
- JavaScript
interface ExampleParam {
name: string;
born: number;
}
export async function example({ name, born }: ExampleParam): Promise<string> {
return `Hello ${name}, you were born in ${born}.`;
}
export async function example({ name, born }) {
return `Hello ${name}, you were born in ${born}.`;
}
Workflow return values
Workflow return values must also be serializable. Returning results, returning errors, or throwing exceptions is fairly idiomatic in each language that is supported. However, Temporal APIs that must be used to get the result of a Workflow Execution will only ever receive one of either the result or the error.
To return a value of the Workflow function, use Promise<something>
. The Promise
is used to make asynchronous calls and comes with guarantees.
The following example uses a Promise<string>
to eventually return a name
and born
parameter.
interface ExampleParam {
name: string;
born: number;
}
export async function example({ name, born }: ExampleParam): Promise<string> {
return `Hello ${name}, you were born in ${born}.`;
}
Workflow Type
Workflows have a Type that are referred to as the Workflow name.
The following examples demonstrate how to set a custom name for your Workflow Type.
In TypeScript, the Workflow Type is the Workflow function name and there isn't a mechanism to customize the Workflow Type.
In the following example, the Workflow Type is the name of the function, helloWorld
.
- TypeScript
- JavaScript
export async function helloWorld(): Promise<string> {
return '👋 Hello World!';
}
export async function helloWorld() {
return '👋 Hello World!';
}
Workflow logic requirements
Workflow logic is constrained by deterministic execution requirementsWhat is a Workflow Definition?
A Workflow Definition is the code that defines the constraints of a Workflow Execution.
Learn more.
Therefore, each language is limited to the use of certain idiomatic techniques.
However, each Temporal SDK provides a set of APIs that can be used inside your Workflow to interact with external (to the Workflow) application code.
In the Temporal TypeScript SDK, Workflows run in a deterministic sandboxed environment. The code is bundled on Worker creation using Webpack, and can import any package as long as it does not reference Node.js or DOM APIs.
If you must use a library that references a Node.js or DOM API and you are certain that those APIs are not used at runtime, add that module to the ignoreModules list.
The Workflow sandbox can run only deterministic code, so side effects and access to external state must be done through Activities because Activity outputs are recorded in the Event History and can read deterministically by the Workflow.
This limitation also means that Workflow code cannot directly import the Activity DefinitionWhat is an Activity Definition?
An Activity Definition is the code that defines the constraints of an Activity Task Execution.
Learn more.
An Activity Definition is the code that defines the constraints of an Activity Task Execution.
Learn more can be imported, so they can be invoked in a type-safe manner.
To make the Workflow runtime deterministic, functions like Math.random()
, Date
, and setTimeout()
are replaced by deterministic versions.
FinalizationRegistry and WeakRef are removed because v8's garbage collector is not deterministic.
Expand to see the implications of the deterministic Date API
import { sleep } from '@temporalio/workflow';
// this prints the *exact* same timestamp repeatedly
for (let x = 0; x < 10; ++x) {
console.log(Date.now());
}
// this prints timestamps increasing roughly 1s each iteration
for (let x = 0; x < 10; ++x) {
await sleep('1 second');
console.log(Date.now());
}
Develop Activities
One of the primary things that Workflows do is orchestrate the execution of Activities.
An Activity is a normal function or method execution that's intended to execute a single, well-defined action (either short or long-running), such as querying a database, calling a third-party API, or transcoding a media file.
An Activity can interact with world outside the Temporal Platform or use a Temporal Client to interact with a Cluster.
For the Workflow to be able to execute the Activity, we must define the Activity DefinitionWhat is an Activity Definition?
An Activity Definition is the code that defines the constraints of an Activity Task Execution.
Learn more.
- Activities execute in the standard Node.js environment.
- Activities cannot be in the same file as Workflows and must be separately registered.
- Activities may be retried repeatedly, so you may need to use idempotency keys for critical side effects.
Activities are just functions. The following is an Activity that accepts a string parameter and returns a string.
- TypeScript
- JavaScript
export async function greet(name: string): Promise<string> {
return `👋 Hello, ${name}!`;
}
export async function greet(name) {
return `👋 Hello, ${name}!`;
}
Activity parameters
There is no explicit limit to the total number of parameters that an Activity DefinitionWhat is an Activity Definition?
An Activity Definition is the code that defines the constraints of an Activity Task Execution.
Learn more may support.
However, there is a limit of the total size of the data ends up encoded into a gRPC message Payload.
A single argument is limited to a maximum size of 2 MB. And the total size of a gRPC message, which includes all the arguments, is limited to a maximum of 4 MB.
Also, keep in mind that all Payload data is recorded in the Workflow Execution Event HistoryWhat is an Event History?
An append log of Events that represents the full state a Workflow Execution.
Learn more and large Event Histories can affect Worker performance.
This is because the entire Event History could be transferred to a Worker Process with a Workflow TaskWhat is a Workflow Task?
A Workflow Task is a Task that contains the context needed to make progress with a Workflow Execution.
Learn more.
Some SDKs require that you pass context objects, others do not. When it comes to your application data—that is, data that is serialized and encoded into a Payload—we recommend that you use a single object as an argument that wraps the application data passed to Activities. This is so that you can change what data is passed to the Activity without breaking a function or method signature.
This Activity takes a single name
parameter of type string
.
- TypeScript
- JavaScript
export async function greet(name: string): Promise<string> {
return `👋 Hello, ${name}!`;
}
export async function greet(name) {
return `👋 Hello, ${name}!`;
}
Activity return values
All data returned from an Activity must be serializable.
There is no explicit limit to the amount of data that can be returned by an Activity, but keep in mind that all return values are recorded in a Workflow Execution Event HistoryWhat is an Event History?
An append log of Events that represents the full state a Workflow Execution.
Learn more.
In TypeScript, the return value is always a Promise.
In the following example, Promise<string>
is the return value.
export async function greet(name: string): Promise<string> {
return `👋 Hello, ${name}!`;
}
Activity Type
Activities have a Type that are referred to as the Activity name. The following examples demonstrate how to set a custom name for your Activity Type.
You can customize the name of the Activity when you register it with the Worker.
In the following example, the Activity Name is activityFoo
.
snippets/src/worker-activity-type-custom.ts
- TypeScript
- JavaScript
import { Worker } from '@temporalio/worker';
import { greet } from './activities';
async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
taskQueue: 'snippets',
activities: {
activityFoo: greet,
},
});
await worker.run();
}
import { Worker } from '@temporalio/worker';
import { greet } from './activities';
async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
taskQueue: 'snippets',
activities: {
activityFoo: greet,
},
});
await worker.run();
}
Activity Execution
Calls to spawn Activity ExecutionsWhat is an Activity Execution?
An Activity Execution is the full chain of Activity Task Executions.
Learn more are written within a Workflow DefinitionWhat is a Workflow Definition?
A Workflow Definition is the code that defines the constraints of a Workflow Execution.
Learn more.
The call to spawn an Activity Execution generates the ScheduleActivityTask Command.
This results in the set of three Activity TaskWhat is an Activity Task?
An Activity Task contains the context needed to make an Activity Task Execution.
Learn more related Events (ActivityTaskScheduled, ActivityTaskStarted, and ActivityTask[Closed])in your Workflow Execution Event History.
A single instance of the Activities implementation is shared across multiple simultaneous Activity invocations. Therefore, the Activity implementation code must be stateless.
The values passed to Activities through invocation parameters or returned through a result value are recorded in the Execution history. The entire Execution history is transferred from the Temporal service to Workflow Workers when a Workflow state needs to recover. A large Execution history can thus adversely impact the performance of your Workflow.
Therefore, be mindful of the amount of data you transfer through Activity invocation parameters or Return Values. Otherwise, no additional limitations exist on Activity implementations.
To spawn an Activity Execution, you must retrieve the Activity handle in your Workflow.
import { proxyActivities } from '@temporalio/workflow';
// Only import the activity types
import type * as activities from './activities';
const { greet } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
});
// A workflow that calls an activity
export async function example(name: string): Promise<string> {
return await greet(name);
}
This imports the individual Activities and declares the type alias for each Activity.
Required timeout
Activity Execution semantics rely on several parameters.
The only required value that needs to be set is either a Schedule-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 or a 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.
These values are set in the Activity Options.
Get Activity results
The call to spawn an Activity ExecutionWhat is an Activity Execution?
An Activity Execution is the full chain of Activity Task Executions.
Learn more generates the ScheduleActivityTask Command and provides the Workflow with an Awaitable.
Workflow Executions can either block progress until the result is available through the Awaitable or continue progressing, making use of the result when it becomes available.
Since Activities are referenced by their string name, you can reference them dynamically to get the result of an Activity Execution.
export async function DynamicWorkflow(activityName, ...args) {
const acts = proxyActivities(/* activityOptions */);
// these are equivalent
await acts.activity1();
await acts['activity1']();
let result = await acts[activityName](...args);
return result;
}
The proxyActivities()
returns an object that calls the Activities in the function. acts[activityName]()
references the Activity using the Activity name, then it returns the results.
Run a dev Worker
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 is where Workflow Functions and Activity Functions are executed.
- Each Worker EntityWhat is a Worker Entity?
A Worker Entity is the individual Worker within a Worker Process that listens to a specific Task Queue.
Learn more in the Worker Process must register the exact Workflow Types and Activity Types it may execute. - Each Worker Entity must also associate itself with exactly one Task QueueWhat is a Task Queue?
A Task Queue is a first-in, first-out queue that a Worker Process polls for Tasks.
Learn more. - Each Worker Entity polling the same Task Queue must be registered with the same Workflow Types and Activity Types.
A Worker EntityWhat is a Worker Entity?
A Worker Entity is the individual Worker within a Worker Process that listens to a specific Task Queue.
Learn more is the component within a Worker Process that listens to a specific Task Queue.
Although multiple Worker Entities can be in a single Worker Process, a single Worker Entity Worker Process may be perfectly sufficient. For more information, see the Worker tuning guide.
A Worker Entity contains both a Workflow Worker and an Activity Worker so that it can make progress for either a Workflow Execution or an Activity Execution.
Create a Worker with Worker.create()
(which establishes the initial gRPC connection), then call worker.run()
on it (to start polling the Task Queue).
Below is an example of starting a Worker that polls the Task Queue named tutorial
.
- TypeScript
- JavaScript
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
async function run() {
// Step 1: Register Workflows and Activities with the Worker and connect to
// the Temporal server.
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'hello-world',
});
// Worker connects to localhost by default and uses console.error for logging.
// Customize the Worker by passing more options to create():
// https://typescript.temporal.io/api/classes/worker.Worker
// If you need to configure server connection parameters, see docs:
// https://docs.temporal.io/typescript/security#encryption-in-transit-with-mtls
// Step 2: Start accepting tasks on the `hello-world` queue
await worker.run();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
async function run() {
// Step 1: Register Workflows and Activities with the Worker and connect to
// the Temporal server.
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'hello-world',
});
// Worker connects to localhost by default and uses console.error for logging.
// Customize the Worker by passing more options to create():
// https://typescript.temporal.io/api/classes/worker.Worker
// If you need to configure server connection parameters, see docs:
// https://docs.temporal.io/typescript/security#encryption-in-transit-with-mtls
// Step 2: Start accepting tasks on the `hello-world` queue
await worker.run();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
taskQueue
is the only required option, but you will also use workflowsPath
and activities
to register Workflows and Activities with the Worker.
A full example for Workers looks like this:
import { NativeConnection, Worker } from '@temporalio/worker';
import * as activities from './activities';
async function run() {
const connection = await NativeConnection.connect({
// defaults port to 7233 if not specified
address: 'foo.bar.tmprl.cloud',
tls: {
// set to true if TLS without mTLS
// See docs for other TLS options
clientCertPair: {
crt: clientCert,
key: clientKey,
},
},
});
const worker = await Worker.create({
connection,
namespace: 'foo.bar', // as explained in Namespaces section
// ...
});
await worker.run();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
See below for more Worker options.
Workflow and Activity registration
Workers bundle Workflow code and node_modules
using Webpack v5 and execute them inside V8 isolates.
Activities are directly required and run by Workers in the Node.js environment.
Workers are very flexible – you can host any or all of your Workflows and Activities on a Worker, and you can host multiple Workers in a single machine.
There are three main things the Worker needs:
taskQueue
: the Task Queue to poll. This is the only required argument.activities
: Optional. Imported and supplied directly to the Worker. Not the path.- Workflow bundle:
- Either specify a
workflowsPath
to yourworkflows.ts
file to pass to Webpack, e.g.,require.resolve('./workflows')
. Workflows will be bundled with their dependencies. - Or pass a prebuilt bundle to
workflowBundle
instead if you prefer to handle the bundling yourself.
Additional Worker Options
This is a selected subset of options you are likely to use. Even more advanced options, particularly for performance tuning, are available in the API reference.
Options | Description |
---|---|
dataConverter | Encodes and decodes data entering and exiting a Temporal Server. Supports undefined , UintBArray , and JSON. |
sinks | Allows injection of Workflow Sinks (Advanced feature: see Logging docs) |
interceptors | A mapping of interceptor type to a list of factories or module paths (Advanced feature: see Interceptors) |
Operation guides:
Run a Worker on Docker
Workers based on the TypeScript SDK can be deployed and run as Docker containers.
At this moment, we recommend using Node.js 18.
(Node.js 20 has known issues.)
Both amd64
and arm64
platforms are supported.
A glibc-based image is required; musl-based images are not supported (see below).
The easiest way to deploy a TypeScript SDK Worker on Docker is to start with the node:18-bullseye
image.
For example:
FROM node:18-bullseye
COPY . /app
WORKDIR /app
RUN npm install --only=production \
&& npm run build
CMD ["build/worker.js"]
For smaller images and/or more secure deployments, it is also possible to use -slim
Docker image variants (like node:18-bullseye-slim
) or distroless/nodejs
Docker images (like gcr.io/distroless/nodejs:18
) with the following caveats.
Using node:slim
images
node:slim
images do not contain some of the common packages found in regular images. This results in significantly smaller images.
However, TypeScript SDK requires the presence of root TLS certificates (the ca-certificates
package), which are not included in slim
images.
The ca-certificates
package is required even when connecting to a local Temporal Server or when using a server connection config that doesn't explicitly use TLS.
For this reason, the ca-certificates
package must be installed during the construction of the Docker image.
For example:
FROM node:18-bulleyes-slim
RUN apt-get update \
&& apt-get install -y ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# ... same as with regular image
Failure to install this dependency results in a [TransportError: transport error]
runtime error, because the certificates cannot be verified.
Using distroless/nodejs
images
distroless/nodejs
images include only the files that are strictly required to execute node
.
This results in even smaller images (approximately half the size of node:slim
images).
It also significantly reduces the surface of potential security issues that could be exploited by a hacker in the resulting Docker images.
It is generally possible and safe to execute TypeScript SDK Workers using distroless/nodejs
images (unless your code itself requires dependencies that are not included in distroless/nodejs
).
However, some tools required for the build process (notably the npm
command) are not included in the distroless/nodejs
image.
This might result in various error messages during the Docker build.
The recommanded solution is to use a multi-step Dockerfile. For example:
# -- BUILD STEP --
FROM node:18-bulleyes AS builder
COPY . /app
WORKDIR /app
RUN npm install --only=production \
&& npm run build
# -- RESULTING IMAGE --
FROM gcr.io/distroless/nodejs:18
COPY --from=builder /app /app
WORKDIR /app
CMD ["build/worker.js"]
Properly configure Node.js memory in Docker
By default, node
configures its maximum old-gen memory to 25% of the physical memory of the machine on which it is executing, with a maximum of 4 GB.
This is likely inappropriate when running Node.js in a Docker environment and can result in either underusage of available memory (node
only uses a fraction of the memory allocated to the container) or overusage (node
tries to use more memory than what is allocated to the container, which will eventually lead to the process being killed by the operating system).
Therefore we recommended that you always explicitly set the --max-old-space-size
node
argument to approximately 80% of the maximum size (in megabytes) that you want to allocate the node
process.
You might need some experimentation and adjustment to find the most appropriate value based on your specific application.
In practice, it is generally easier to provide this argument through the NODE_OPTIONS
environment variable.
Do not use Alpine
Alpine replaces glibc with musl, which is incompatible with the Rust core of the TypeScript SDK. If you receive errors like the following, it's probably because you are using Alpine.
Error: Error loading shared library ld-linux-x86-64.so.2: No such file or directory (needed by /opt/app/node_modules/@temporalio/core-bridge/index.node)
Or like this:
Error: Error relocating /opt/app/node_modules/@temporalio/core-bridge/index.node: __register_atfork: symbol not found
Run a Temporal Cloud Worker
To run a Worker that uses Temporal Cloud, you need to provide additional connection and client options that include the following:
- An address that includes your Cloud Namespace NameWhat is a Namespace?
A Namespace is a unit of isolation within the Temporal Platform
Learn more and a port number:<Namespace>.<ID>.tmprl.cloud:<port>
. - mTLS CA certificate.
- mTLS private key.
For more information about managing and generating client certificates for Temporal Cloud, see How to manage certificates in Temporal Cloud.
For more information about configuring TLS to secure inter- and intra-network communication for a Temporal Cluster, see Temporal Customization Samples.
Register types
All Workers listening to the same Task Queue name must be registered to handle the exact same Workflows Types and Activity Types.
If a Worker polls a Task for a Workflow Type or Activity Type it does not know about, it fails that Task. However, the failure of the Task does not cause the associated Workflow Execution to fail.
In development, use workflowsPath
:
- TypeScript
- JavaScript
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
taskQueue: 'snippets',
activities,
});
await worker.run();
}
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
taskQueue: 'snippets',
activities,
});
await worker.run();
}
In this snippet, the Worker bundles the Workflow code at runtime.
In production, you can improve your Worker's startup time by bundling in advance: as part of your production build, call bundleWorkflowCode
:
production/src/scripts/build-workflow-bundle.ts
- TypeScript
- JavaScript
import { bundleWorkflowCode } from '@temporalio/worker';
import { writeFile } from 'fs/promises';
import path from 'path';
async function bundle() {
const { code } = await bundleWorkflowCode({
workflowsPath: require.resolve('../workflows'),
});
const codePath = path.join(__dirname, '../../workflow-bundle.js');
await writeFile(codePath, code);
console.log(`Bundle written to ${codePath}`);
}
import { bundleWorkflowCode } from '@temporalio/worker';
import { writeFile } from 'fs/promises';
import path from 'path';
async function bundle() {
const { code } = await bundleWorkflowCode({
workflowsPath: require.resolve('../workflows'),
});
const codePath = path.join(__dirname, '../../workflow-bundle.js');
await writeFile(codePath, code);
console.log(`Bundle written to ${codePath}`);
}
Then the bundle can be passed to the Worker:
- TypeScript
- JavaScript
const workflowOption = () =>
process.env.NODE_ENV === 'production'
? {
workflowBundle: {
codePath: require.resolve('../workflow-bundle.js'),
},
}
: { workflowsPath: require.resolve('./workflows') };
async function run() {
const worker = await Worker.create({
...workflowOption(),
activities,
taskQueue: 'production-sample',
});
await worker.run();
}
const workflowOption = () => process.env.NODE_ENV === 'production'
? {
workflowBundle: {
codePath: require.resolve('../workflow-bundle.js'),
},
}
: { workflowsPath: require.resolve('./workflows') };
async function run() {
const worker = await Worker.create({
...workflowOption(),
activities,
taskQueue: 'production-sample',
});
await worker.run();
}
Start Workflow Execution
Workflow Execution semantics rely on several parameters—that is, to start a Workflow Execution you must supply a Task Queue that will be used for the Tasks (one that a Worker is polling), the Workflow Type, language-specific contextual data, and Workflow Function parameters.
In the examples below, all Workflow Executions are started using a Temporal Client. To spawn Workflow Executions from within another Workflow Execution, use either the Child Workflow or External Workflow APIs.
See the Customize Workflow Type section to see how to customize the name of the Workflow Type.
A request to spawn a Workflow Execution causes the Temporal Cluster to create the first Event (WorkflowExecutionStarted) in the Workflow Execution Event History. The Temporal Cluster then creates the first Workflow Task, resulting in the first WorkflowTaskScheduled Event.
When you have a Workflow Client, you can schedule the start of a Workflow with client.start()
, specifying workflowId
, taskQueue
, and args
and returning a Workflow handle immediately after the Server acknowledges the receipt.
const handle = await client.start(example, {
workflowId: 'your-workflow-id',
taskQueue: 'your-task-queue',
args: ['argument01', 'argument02', 'argument03'], // this is typechecked against workflowFn's args
});
const handle = client.getHandle(workflowId);
const result = await handle.result();
Calling client.start()
and client.execute()
send a command to Temporal Server to schedule a new Workflow Execution on the specified Task Queue. It does not actually start until a Worker that has a matching Workflow Type, polling that Task Queue, picks it up.
You can test this by executing a Workflow Client command without a matching Worker. Temporal Server records the command in Event History, but does not make progress with the Workflow Execution until a Worker starts polling with a matching Task Queue and Workflow Definition.
Workflow Execution run in a separate V8 isolate context in order to provide a deterministic runtime.
Set Task Queue
In most SDKs, the only Workflow Option that must be set is the name of the Task QueueWhat is a Task Queue?
A Task Queue is a first-in, first-out queue that a Worker Process polls for Tasks.
Learn more.
For any code to execute, a Worker Process must be running that contains a Worker Entity that is polling the same Task Queue name.
A Task Queue is a dynamic queue in Temporal polled by one or more Workers.
Workers bundle Workflow code and node modules using Webpack v5 and execute them inside V8 isolates. Activities are directly required and run by Workers in the Node.js environment.
Workers are flexible. You can host any or all of your Workflows and Activities on a Worker, and you can host multiple Workers on a single machine.
There are three main things the Worker needs:
taskQueue
: the Task Queue to poll. This is the only required argument.activities
: Optional. Imported and supplied directly to the Worker.- Workflow bundle, specify one of the following options:
- a
workflowsPath
to yourworkflows.ts
file to pass to Webpack. For example,require.resolve('./workflows')
. Workflows will be bundled with their dependencies. - Or pass a prebuilt bundle to
workflowBundle
, if you prefer to handle the bundling yourself.
- a
- TypeScript
- JavaScript
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
async function run() {
// Step 1: Register Workflows and Activities with the Worker and connect to
// the Temporal server.
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'hello-world',
});
// Worker connects to localhost by default and uses console.error for logging.
// Customize the Worker by passing more options to create():
// https://typescript.temporal.io/api/classes/worker.Worker
// If you need to configure server connection parameters, see docs:
// /typescript/security#encryption-in-transit-with-mtls
// Step 2: Start accepting tasks on the `tutorial` queue
await worker.run();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
async function run() {
// Step 1: Register Workflows and Activities with the Worker and connect to
// the Temporal server.
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'hello-world',
});
// Worker connects to localhost by default and uses console.error for logging.
// Customize the Worker by passing more options to create():
// https://typescript.temporal.io/api/classes/worker.Worker
// If you need to configure server connection parameters, see docs:
// /typescript/security#encryption-in-transit-with-mtls
// Step 2: Start accepting tasks on the `tutorial` queue
await worker.run();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
taskQueue
is the only required option; however, use workflowsPath
and activities
to register Workflows and Activities with the Worker.
When scheduling a Workflow, a taskQueue
must be specified.
- TypeScript
- JavaScript
import { Connection, WorkflowClient } from '@temporalio/client';
// This is the code that is used to start a workflow.
const connection = await Connection.create();
const client = new WorkflowClient({ connection });
const result = await client.execute(yourWorkflow, {
// required
taskQueue: 'your-task-queue',
// required
workflowId: 'your-workflow-id',
});
import { Connection, WorkflowClient } from '@temporalio/client';
// This is the code that is used to start a workflow.
const connection = await Connection.create();
const client = new WorkflowClient({ connection });
const result = await client.execute(yourWorkflow, {
// required
taskQueue: 'your-task-queue',
// required
workflowId: 'your-workflow-id',
});
When creating a Worker, you must pass the taskQueue
option to the Worker.create()
function.
- TypeScript
- JavaScript
const worker = await Worker.create({
// imported elsewhere
activities,
taskQueue: 'your-task-queue',
});
const worker = await Worker.create({
// imported elsewhere
activities,
taskQueue: 'your-task-queue',
});
Optionally, in Workflow code, when calling an Activity, you can specify the Task Queue by passing the taskQueue
option to proxyActivities()
, startChild()
, or executeChild()
. If you do not specify a taskQueue
, then the TypeScript SDK places Activity and Child Workflow Tasks in the same Task Queue as the Workflow Task Queue.
Workflow Id
Although it is not required, we recommend providing your own Workflow IdWhat is a Workflow Id?
A Workflow Id is a customizable, application-level identifier for a Workflow Execution that is unique to an Open Workflow Execution within a Namespace.
Learn more that maps to a business process or business entity identifier, such as an order identifier or customer identifier.
Connect to a Client with client.start()
and any arguments. Then specify your taskQueue
and set your workflowId
to a meaningful business identifier.
const handle = await client.start(example, {
workflowId: 'yourWorkflowId',
taskQueue: 'yourTaskQueue',
args: ['your', 'arg', 'uments'],
});
This starts a new Client with the given Workflow Id, Task Queue name, and an argument.
Get Workflow results
If the call to start a Workflow Execution is successful, you will gain access to the Workflow Execution's Run Id.
The Workflow Id, Run Id, and Namespace may be used to uniquely identify a Workflow Execution in the system and get its result.
It's possible to both block progress on the result (synchronous execution) or get the result at some other point in time (asynchronous execution).
In the Temporal Platform, it's also acceptable to use Queries as the preferred method for accessing the state and results of Workflow Executions.
To return the results of a Workflow Execution:
return (
'Completed '
+ wf.workflowInfo().workflowId
+ ', Total Charged: '
+ totalCharged
);
totalCharged
is just a function declared in your code. For a full example, see subscription-workflow-project-template-typescript/src/workflows.ts.
A Workflow function may return a result. If it doesn’t (in which case the return type is Promise<void>
), the result will be undefined
.
If you started a Workflow with handle.start()
, you can choose to wait for the result anytime with handle.result()
.
const handle = client.getHandle(workflowId);
const result = await handle.result();
Using a Workflow Handle isn't necessary with client.execute()
.
Workflows that prematurely end will throw a WorkflowFailedError
if you call result()
.
If you call result()
on a Workflow that prematurely ended for some reason, it throws a WorkflowFailedError
error that reflects the reason. For that reason, it is recommended to catch that error.
const handle = client.getHandle(workflowId);
try {
const result = await handle.result();
} catch (err) {
if (err instanceof WorkflowFailedError) {
throw new Error('Temporal workflow failed: ' + workflowId, {
cause: err,
});
} else {
throw new Error('error from Temporal workflow ' + workflowId, {
cause: err,
});
}
}