Core application - TypeScript SDK
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.
Topics covered in this section
How to install the Temporal CLI and run a development server
This section describes how to install the Temporal CLI and run a development Temporal Service. The local development Temporal Service comes packaged with the Temporal Web UI.
For information on deploying and running a self-hosted production Temporal Service, see the Self-hosted guide, or sign up for Temporal Cloud and let us run your production Temporal Service for you.
Temporal CLI is a tool for interacting with a Temporal Service from the command line and it includes a distribution of the Temporal Server and Web UI. This local development Temporal Service 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
The Temporal CLI is available on macOS, Windows, and Linux.
macOS
How to install the Temporal CLI on macOS
Choose one of the following install methods to install the Temporal CLI on macOS:
Install the Temporal CLI with Homebrew
brew install temporal
Install the Temporal CLI from CDN
- Select the platform and architecture needed.
- Download for Darwin amd64: https://temporal.download/cli/archive/latest?platform=darwin&arch=amd64
- Download for Darwin arm64: https://temporal.download/cli/archive/latest?platform=darwin&arch=arm64
-
Extract the downloaded archive.
-
Add the
temporal
binary to your PATH.
Linux
How to install the Temporal CLI on Linux
Choose one of the following install methods to install the Temporal CLI on Linux:
Install the Temporal CLI with Homebrew
brew install temporal
Install the Temporal CLI from CDN
- Select the platform and architecture needed.
- Download for Linux amd64: https://temporal.download/cli/archive/latest?platform=linux&arch=amd64
- Download for Linux arm64: https://temporal.download/cli/archive/latest?platform=linux&arch=arm64
-
Extract the downloaded archive.
-
Add the
temporal
binary to your PATH.
Windows
How to install the Temporal CLI on Windows
Follow these instructions to install the Temporal CLI on Windows:
Install the Temporal CLI from CDN
- Select the platform and architecture needed and download the binary.
- Download for Windows amd64: https://temporal.download/cli/archive/latest?platform=windows&arch=amd64
- Download for Windows arm64: https://temporal.download/cli/archive/latest?platform=windows&arch=arm64
-
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 Service.
- 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 Service
A Temporal Client enables you to communicate with the Temporal Service. Communication with a Temporal Service 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 Service.
When you are running a Temporal Service 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 Service, 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 Service, 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 to 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 Temporal Service. 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 `👋 Hello, ${name}!`;
}
How to customize your 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
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();
}
Important design patterns for Activities
The following are some important (and frequently requested) patterns for using our Activities APIs. These patterns address common needs and use cases.
Share dependencies in Activity functions (dependency injection)
Because Activities are "just functions," you can also create functions that create Activities. This is a helpful pattern for using closures to do the following:
- Store expensive dependencies for sharing, such as database connections.
- Inject secret keys (such as environment variables) from the Worker to the Activity.
activities-dependency-injection/src/activities.ts
export interface DB {
get(key: string): Promise<string>;
}
export const createActivities = (db: DB) => ({
async greet(msg: string): Promise<string> {
const name = await db.get('name'); // simulate read from db
return `${msg}: ${name}`;
},
async greet_es(mensaje: string): Promise<string> {
const name = await db.get('name'); // simulate read from db
return `${mensaje}: ${name}`;
},
});
See full example
When you register these in the Worker, pass your shared dependencies accordingly:
import { createActivities } from './activities';
async function run() {
// Mock DB connection initialization in Worker
const db = {
async get(_key: string) {
return 'Temporal';
},
};
const worker = await Worker.create({
taskQueue: 'dependency-injection',
workflowsPath: require.resolve('./workflows'),
activities: createActivities(db),
});
await worker.run();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
Because Activities are always referenced by name, inside the Workflow they can be proxied as normal, although the types need some adjustment:
activities-dependency-injection/src/workflows.ts
import type { createActivities } from './activities';
// Note usage of ReturnType<> generic since createActivities is a factory function
const { greet, greet_es } = proxyActivities<
ReturnType<typeof createActivities>
>({
startToCloseTimeout: '30 seconds',
});
Import multiple Activities simultaneously
You can proxy multiple Activities from the same proxyActivities
call if you want them to share the same timeouts, retries, and options:
export async function Workflow(name: string): Promise<string> {
// destructuring multiple activities with the same options
const { act1, act2, act3 } = proxyActivities<typeof activities>();
/* activityOptions */
await act1();
await Promise.all([act2, act3]);
}
Dynamically reference Activities
Because Activities are referenced only by their string names, you can reference them dynamically if needed:
export async function DynamicWorkflow(activityName, ...args) {
const acts = proxyActivities(/* activityOptions */);
// these are equivalent
await acts.activity1();
await acts['activity1']();
// dynamic reference to activities using activityName
let result = await acts[activityName](...args);
}
Type safety is still supported here, but we encourage you to validate and handle mismatches in Activity names.
An invalid Activity name leads to a NotFoundError
with a message that looks like this:
ApplicationFailure: Activity function actC is not registered on this Worker, available activities: ["actA", "actB"]
How to start an Activity Execution
Calls to spawn Activity Executions are written within a Workflow Definition. The call to spawn an Activity Execution generates the ScheduleActivityTask Command. This results in the set of three Activity Task 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. Activity implementation code should be idempotent.
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.