Foundations - TypeScript SDK feature guide
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 Application—that is, all the relevant steps to start a Workflow Execution that executes an Activity.
In this section you can find the following:
- Run a development Cluster
- Connect to a dev Cluster
- Connect to Temporal Cloud
- Develop a Workflow
- Develop an Activity
- Start an Activity Execution
- Run a dev Worker
- Run a Worker on Docker
- Run a Temporal Cloud Worker
- Start a Workflow Execution
How to install the Temporal CLI and 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 self-hosted production Cluster, see the Self-hosted 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
How to install a Temporal SDK
A Temporal SDK provides a framework for Temporal Application development.
An SDK provides you with the following:
- A Temporal Client to communicate with a Temporal Cluster.
- APIs to develop Workflows.
- APIs to create and manage Worker Processes.
- APIs to author Activities.
This project requires Node.js 16.15 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.
How to find the TypeScript SDK API reference
The Temporal TypeScript SDK API reference is published to typescript.temporal.io.
Where are SDK-specific code examples?
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.
How to import an ECMAScript module
The JavaScript ecosystem is quickly moving toward publishing ECMAScript modules (ESM) instead of CommonJS modules.
For example, node-fetch@3
is ESM, but node-fetch@2
is CommonJS.
For more information about importing a pure ESM dependency, see our Fetch ESM sample for the necessary configuration changes:
package.json
must have include the"type": "module"
attribute.tsconfig.json
should output inesnext
format.- Imports must include the
.js
file extension.
Linting and types in TypeScript
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.
How to connect a Temporal Client to a Temporal Cluster
A Temporal Client enables you to communicate with the Temporal Cluster. 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 the Temporal CLI), 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
.
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);
});
How to connect to Temporal Cloud
When you connect to Temporal Cloud, you need to provide additional connection and client options that include the following:
- The Temporal Cloud Namespace Id.
- The Namespace's gRPC endpoint. An endpoint listing is available at the Temporal Cloud Website on each Namespace detail page. The endpoint contains the Namespace Id and 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.
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);
});
How 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.
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 };
}
How to define 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.workflow.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.workflow.start()
and its resulting value.
interface ExampleParam {
name: string;
born: number;
}
export async function example({ name, born }: ExampleParam): Promise<string> {
return `Hello ${name}, you were born in ${born}.`;
}
How to define Workflow return parameters
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}.`;
}
How to customize your 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
.
export async function helloWorld(): Promise<string> {
return '👋 Hello World!';
}
How develop Workflow logic
Workflow logic is constrained by deterministic execution requirements. 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 Definition. Activity Types 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());
}
How to develop a basic Activity
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 Definition.
- 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.
export async function greet(name: string): Promise<string> {
return `👋 Hello, ${name}!`;
}
How to develop Activity Parameters
There is no explicit limit to the total number of parameters that an Activity Definition may support. However, there is a limit to the total size of the data that 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 History 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 Task.
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
.
export async function greet(name: string): Promise<string> {
return `👋 Hello, ${name}!`;
}
How to define 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 History.
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 `