
Build a Temporal Application from scratch in TypeScript
- Build the application
- Test and run a Worker
- Run and observe retries
In this tutorial, you'll build your first Temporal Application from scratch using the Temporal TypeScript SDK. You'll develop a small application that asks for your name and then uses APIs to get your public IP address and your location based on that address. External requests can fail due to rate limiting, network interruptions, or other errors. Using Temporal for this application will let you automatically recover from these and other kinds of failures without having to write explicit error-handling code.
The app will consist of the following pieces:
- Two Activities: the first gets your IP address, and the second uses that IP to find your location.
- A Workflow that calls both Activities, using the result of the first as input to the second.
- A Worker to host the Workflow and Activity code.
- A client program to start your Workflow.
You'll also write tests to verify your Workflow runs successfully.
Prerequisites
Before starting this tutorial:
- Set up a local development environment for developing Temporal Applications using Node.js and TypeScript. Ensure the Temporal Service is running locally and you can access the Web UI on port
8233(the default). - Follow the Run your first Temporal application with the TypeScript SDK tutorial to understand how Temporal's components fit together.
Create a new Temporal TypeScript project
While you could create a new directory, initialize a TypeScript project, and configure things manually, the TypeScript SDK offers a project creation tool you can use to create a project folder and set up dependencies.
Run the following command in your shell:
npx @temporalio/create --sample empty temporal-ip-geolocation
The command will ask you to confirm if you want to install @temporalio/create:
Need to install the following packages:
@temporalio/create@1.11.5
Ok to proceed? (y) y
Enter y to continue. The process will then create your app:
Creating a new Temporal project in /Users/temporal/temporal-ip-geolocation/
Downloading files for sample empty. This might take a moment.
Installing packages. This might take a couple of minutes.
It'll ask you if you'd like to create a Git repository:
✔ Would you like me to initialize a git repository for the project? … yes
Initialized a git repository.
Then it'll give you further instructions, including how to set up and start a local Temporal Service and install a compatible version of Node.js:
Success! Created project temporal-ip-geolocation at:
/Users/temporal/temporal-ip-geolocation/
To begin development, install the Temporal CLI:
Mac: brew install temporal
Other: Download and extract the latest release from https://github.com/temporalio/cli/releases/latest
Start Temporal Server:
temporal server start-dev
Use Node version 18+ (v22.x is recommended):
Mac: brew install node@22
Other: https://nodejs.org/en/download/
Finally, you'll see how to run the Worker and Workflow, but don't do that yet.
Once the command completes, switch to the temporal-ip-geolocation folder:
cd temporal-ip-geolocation
The generator created the following files and folders:
├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── src
│ ├── activities.ts
│ ├── client.ts
│ ├── mocha
│ ├── worker.ts
│ └── workflows.ts
└── tsconfig.json
Here's what each file does:
- The
package.jsonfile holds the project dependencies and a handful of scripts you'll use to run Workflows, tests, and other tasks like linting and formatting your code. - The
tsconfig.jsonfile holds the TypeScript configuration designed for working with Temporal's SDK. - The
src/activities.tsfile is where you can define Activities. - The
src/client.tsfile has the code for a small CLI program to execute a Workflow. You won't use this directly in this guide. - The
src/mochafolder is where you'll place your tests. We recommend using Mocha to test your Temporal Workflows and Activities. - The
src/workflows.tsfile is where you can define Workflows. - The
src/worker.tsfile has the code to configure and run your Worker process, which executes your Workflows and Activities.
You should review a few parts of the package.json before moving on.
First, review the scripts section. These are the npm commands you'll use to build, lint, test, and start your application code:
"scripts": {
"build": "tsc --build",
"build.watch": "tsc --build --watch",
"lint": "eslint .",
"start": "ts-node src/worker.ts",
"start.watch": "nodemon src/worker.ts",
"workflow": "ts-node src/client.ts",
"format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts"
},
Next, examine the packages listed as dependencies. These are the packages that compose the Temporal TypeScript SDK, and each package maps to the four parts of a Temporal application: an Activity, Client, Worker, and Workflow. There is also Nanoid, an npm package which you'll use to generate a unique identifier for your Workflow.
"dependencies": {
"@temporalio/activity": "^1.11.5",
"@temporalio/client": "^1.11.5",
"@temporalio/worker": "^1.11.5",
"@temporalio/workflow": "^1.11.5",
"nanoid": "3.x"
},
You'll use Node.js's built-in fetch library in your application. To use it with TypeScript, open the file tsconfig.json and locate the "lib" key under "compilerOptions" and add the DOM library to the array:
"compilerOptions": {
...
"lib": ["es2020","DOM"],
...
}
This ensures that fetch will be available as a global module.
With the project created, you'll create the application's core logic.
Write functions to call external services
Your application will make two HTTP requests. The first request will return your current public IP, while the second request will use that IP to provide city, state, and country information.
You'll use Temporal Activities to make these requests. You use Activities in your Temporal Applications to execute non-deterministic code or perform operations that may fail.
If an Activity fails, Temporal can automatically retry it until it succeeds or reaches a specified retry limit. This ensures that transient issues, like network glitches or temporary service outages, don't result in data loss or incomplete processes.
Open src/activities.ts and remove all the existing code in the file. You'll replace it with your own.
Add the following code to define a Temporal Activity that retrieves your IP address from icanhazip.com:
// Get the IP address
export async function getIP(): Promise<string> {
const url = 'https://icanhazip.com';
const response = await fetch(url);
const data = await response.text();
return data.trim();
}
This function looks like a regular TypeScript function. With the Temporal TypeScript SDK, you define Activities using an exportable TypeScript module.
The response from icanhazip.com is plain-text, and it includes a newline, so you trim off the newline character before returning the result.
Notice that there's no error-handling code in this function. When you build your Workflow, you'll use Temporal's Activity Retry policies to retry this code automatically if there's an error.
Now add the second Activity that accepts an IP address and retrieves location data. In src/activities.ts, add the following code to define it:
// Use the IP address to get the location.
export async function getLocationInfo(ip: string): Promise<string> {
const url = `http://ip-api.com/json/${ip}`;
const response = await fetch(url);
const data = await response.json();
return `${data.city}, ${data.regionName}, ${data.country}`;
}
This Activity follows the same pattern as the getIP Activity. It's an exported async function that uses fetch to call a remote service. This time, the service returns JSON data rather than text.
While Activities can accept input arguments, it's a best practice to send a single argument rather than multiple arguments. In this case you only have a single string. If you have more than one argument, bundle them up in a serializable object. Review the Activity parameters section of the Temporal documentation for details.
You've created your two Activities. Now you'll coordinate them using a Temporal Workflow.
Control application logic with a Workflow
Workflows are where you configure and organize the execution of Activities. You define a Workflow by writing a Workflow Definition using one of the Temporal SDKs. Review the Develop Workflows section of the Temporal documentation for more about Workflows in TypeScript.
Open the file src/workflows.ts and remove the code in the file since you'll add your own. Then add the following code to import the Activities and configure how the Workflow should handle failures with a Retry Policy.
import * as workflow from '@temporalio/workflow';
// Only import the activity types
import type * as activities from './activities';
// Load Activities and assign the Retry Policy
const { getIP, getLocationInfo} = workflow.proxyActivities<typeof activities>({
retry: {
initialInterval: '1 second', // amount of time that must elapse before the first retry occurs.
maximumInterval: '1 minute', // maximum interval between retries.
backoffCoefficient: 2, // how much the retry interval increases.
// maximumAttempts: 5, // maximum number of execution attempts. Unspecified means unlimited retries.
},
startToCloseTimeout: '1 minute', // maximum time allowed for a single Activity Task Execution.
});
The Temporal TypeScript SDK requires that Workflows and Activities run in separate environments. Temporal Workflows must be deterministic so that Temporal can replay your Workflow in the event of a crash, and the TypeScript SDK runs Workflows in a sandbox that checks code for determinism to enforce this.
Since you run your non-deterministic operations in Activities, you configure your Workflow to call Activities through a proxy. That's why you import their types rather than the functions themselves.
The proxyActivities method is also where you set options for how Temporal works with Activities. In this example, you have specified that the Start-to-Close Timeout for your Activity will be one minute, meaning that your Activity has one minute to complete before it times out. Of all the Temporal timeout options, startToCloseTimeout is the one you should always set.
You also set the Retry Policy for Activities this way. In this example, you're using the default Retry Policy values, so you don't need to specify the values, but leaving them in gives you a clearer picture of what happens. Note that maximumAttempts is commented out, which means there's no limit to the number of times Temporal will retry your Activities if they fail.
With the imports and options in place, you can define the Workflow itself. In the TypeScript SDK, you implement a Workflow the same way you define an Activity: using an exportable async TypeScript function. Add the following code to call both Activities, using the value of the first as the input to the second:
// The Temporal Workflow.
// Just a TypeScript function.
export async function getAddressFromIP(name: string): Promise<string> {
try {
const ip = await getIP();
try {
const location = await getLocationInfo(ip);
return `Hello, ${name}. Your IP is ${ip} and your location is ${location}`;
} catch (e) {
throw new workflow.ApplicationFailure("Failed to get location");
}
} catch (e) {
throw new workflow.ApplicationFailure("Failed to get IP");
}
}
This code uses a try/catch block, but because you've configured unlimited retries, there won't be any exceptions caught. However, if you change the Retry Policy's maximum retries, or you specify non-retryable exceptions, this code will be in place to handle those errors.
Next you'll create a Worker that executes the Workflow and Activities, and write tests to confirm everything works as expected.
Get notified when we launch new educational content
New courses, tutorials, and learning resources - straight to your inbox.