Skip to main content
Temporal TypeScript SDK

Run your first Temporal application with the TypeScript SDK

~15 minutes totalTemporal beginnerHands-on tutorial
  1. Understand the application
  2. Run the application
  3. Simulate failures

In this tutorial, you'll run your first Temporal Application using the TypeScript SDK. You'll use the Web UI for state visibility, then explore how Temporal helps you recover from common failures.

What you'll do
  • Explore Temporal's core terminology and concepts.
  • Run a Temporal Workflow Application using a Temporal Cluster and the TypeScript SDK.
  • Practice reviewing the state of the Workflow.
  • Understand the inherent reliability of Workflow functions.

Prerequisites

Before starting this tutorial:

Application overview

The project simulates a money transfer application: withdrawals, deposits, and refunds. Money comes out of one account and goes into another. If the withdrawal succeeds but the deposit fails, the money needs to go back to the original account.

The following diagram illustrates what happens when you start the Workflow:

High level project design

The Temporal Server doesn't run your code. Your Worker, Workflow, and Activity run on your infrastructure, along with the rest of your applications.

Download the example application

The application is available in a GitHub repository. Clone it:

git clone https://github.com/temporalio/money-transfer-project-template-ts/
cd money-transfer-project-template-ts
tip

The repository is a GitHub Template, so you can clone it to your own account and use it as the foundation for your own Temporal application.

Workflow Definition

A Workflow Definition in TypeScript is a regular TypeScript function that accepts some input values:

src/workflows.ts
import { proxyActivities } from '@temporalio/workflow';
import { ApplicationFailure } from '@temporalio/common';

import type * as activities from './activities';
import type { PaymentDetails } from './shared';

export async function moneyTransfer(details: PaymentDetails): Promise<string> {
const { withdraw, deposit, refund } = proxyActivities<typeof activities>({
retry: {
initialInterval: '1 second',
maximumInterval: '1 minute',
backoffCoefficient: 2,
maximumAttempts: 500,
nonRetryableErrorTypes: ['InvalidAccountError', 'InsufficientFundsError'],
},
startToCloseTimeout: '1 minute',
});

let withdrawResult: string;
try {
withdrawResult = await withdraw(details);
} catch (withdrawErr) {
throw new ApplicationFailure(`Withdrawal failed. Error: ${withdrawErr}`);
}

let depositResult: string;
try {
depositResult = await deposit(details);
} catch (depositErr) {
let refundResult;
try {
refundResult = await refund(details);
throw ApplicationFailure.create({
message: `Failed to deposit into account ${details.targetAccount}. Money returned to ${details.sourceAccount}.`,
});
} catch (refundErr) {
throw ApplicationFailure.create({
message: `Failed to deposit into account ${details.targetAccount}. Refund failed.`,
});
}
}
return `Transfer complete (transaction IDs: ${withdrawResult}, ${depositResult})`;
}

The moneyTransfer function takes transaction details, executes Activities, and returns the result. The PaymentDetails input type is defined in shared.ts:

src/shared.ts
export type PaymentDetails = {
amount: number;
sourceAccount: string;
targetAccount: string;
referenceId: string;
};
tip

It's a good practice to send a single, serializable data structure into a Workflow as its input.

Activity Definition

Activities are where you perform the business logic. The withdraw Activity calls a service to process the withdrawal:

src/activities.ts
import type { PaymentDetails } from './shared';
import { BankingService } from './banking-client';

export async function withdraw(details: PaymentDetails): Promise<string> {
console.log(`Withdrawing $${details.amount} from account ${details.sourceAccount}.\n\n`);
const bank1 = new BankingService('bank1.example.com');
return await bank1.withdraw(
details.sourceAccount,
details.amount,
details.referenceId
);
}

The deposit Activity looks almost identical:

src/activities.ts
export async function deposit(details: PaymentDetails): Promise<string> {
console.log(`Depositing $${details.amount} into account ${details.targetAccount}.\n\n`);
const bank2 = new BankingService('bank2.example.com');
// Uncomment lines 25-29 and comment lines 30-34 to simulate an unknown failure
// return await bank2.depositThatFails(
// details.targetAccount,
// details.amount,
// details.referenceId
// );
return await bank2.deposit(
details.targetAccount,
details.amount,
details.referenceId
);
}

The commented lines are what you'll use later to simulate a failure.

Why you use Activities

Temporal Workflows have deterministic constraints - they need to be replayable. Use Activities for business logic and Workflows to coordinate.

Set the Retry Policy

If an Activity fails, Temporal Workflows automatically retry. At the top of the Workflow you'll see a Retry Policy:

src/workflows.ts
const { withdraw, deposit, refund } = proxyActivities<typeof activities>({
retry: {
initialInterval: '1 second',
maximumInterval: '1 minute',
backoffCoefficient: 2,
maximumAttempts: 500,
nonRetryableErrorTypes: ['InvalidAccountError', 'InsufficientFundsError'],
},
startToCloseTimeout: '1 minute',
});

By default, Temporal retries failed Activities forever. This example sets a max of 500 attempts and marks InvalidAccountError and InsufficientFundsError as non-retryable.

This is a simplified example

In production you'd add more advanced logic - including a "human in the loop" step where someone is notified of refund issues and can intervene.

Get notified when we launch new educational content

New courses, tutorials, and learning resources - straight to your inbox.

Subscribe
Feedback