A comprehensive NestJS integration framework for Temporal.io that provides enterprise-ready workflow orchestration with automatic discovery, declarative decorators, and robust monitoring capabilities.
NestJS Temporal Core bridges NestJS's powerful dependency injection system with Temporal.io's robust workflow orchestration engine. It provides a declarative approach to building distributed, fault-tolerant applications with automatic service discovery, enterprise-grade monitoring, and seamless integration.
| Feature | Description |
|---|---|
| Seamless Integration | Native NestJS decorators and dependency injection support |
| Auto-Discovery | Automatic registration of activities and workflows via decorators |
| Type Safety | Full TypeScript support with comprehensive type definitions |
| Enterprise Ready | Built-in health checks, monitoring, and error handling |
| Zero Configuration | Smart defaults with extensive customization options |
| Modular Architecture | Use client-only, worker-only, or full-stack configurations |
| Production Grade | Connection pooling, graceful shutdown, and fault tolerance |
@Activity() and @ActivityMethod() for clean, intuitive activity definitionsIWorkflowProxy<T> that infers start args, signal args, and query return types from your workflow function signatureTemporalServicenpm install nestjs-temporal-core @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/common
npm install @nestjs/common @nestjs/core reflect-metadata rxjs
Enable shutdown hooks in your main.ts for proper Temporal resource cleanup:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Required for graceful Temporal connection cleanup
app.enableShutdownHooks();
await app.listen(3000);
}
bootstrap();
Import and configure TemporalModule in your app module:
// app.module.ts
import { Module } from '@nestjs/common';
import { TemporalModule } from 'nestjs-temporal-core';
import { PaymentActivity } from './activities/payment.activity';
import { EmailActivity } from './activities/email.activity';
@Module({
imports: [
TemporalModule.register({
connection: {
address: 'localhost:7233',
namespace: 'default',
},
taskQueue: 'my-task-queue',
worker: {
workflowsPath: require.resolve('./workflows'),
activityClasses: [PaymentActivity, EmailActivity],
autoStart: true,
},
}),
],
providers: [PaymentActivity, EmailActivity],
})
export class AppModule {}
Create activities using @Activity() and @ActivityMethod() decorators:
// payment.activity.ts
import { Injectable } from '@nestjs/common';
import { Activity, ActivityMethod } from 'nestjs-temporal-core';
export interface PaymentData {
amount: number;
currency: string;
customerId: string;
}
@Injectable()
@Activity({ name: 'payment-activities' })
export class PaymentActivity {
@ActivityMethod('processPayment')
async processPayment(data: PaymentData): Promise<{ transactionId: string }> {
// Payment processing logic with full NestJS DI support
console.log(`Processing payment: $${data.amount} ${data.currency}`);
// Simulate payment processing
await new Promise(resolve => setTimeout(resolve, 1000));
return { transactionId: `txn_${Date.now()}` };
}
@ActivityMethod('refundPayment')
async refundPayment(transactionId: string): Promise<{ refundId: string }> {
// Refund logic
console.log(`Refunding transaction: ${transactionId}`);
return { refundId: `ref_${Date.now()}` };
}
}
Create workflows as pure Temporal functions (NOT NestJS services):
// payment.workflow.ts
import { proxyActivities, defineSignal, defineQuery, setHandler } from '@temporalio/workflow';
import type { PaymentActivity } from './payment.activity';
// Create activity proxies
const { processPayment, refundPayment } = proxyActivities<typeof PaymentActivity.prototype>({
startToCloseTimeout: '5m',
retry: {
maximumAttempts: 3,
initialInterval: '1s',
},
});
// Define signals and queries
export const cancelPaymentSignal = defineSignal<[string]>('cancelPayment');
export const getPaymentStatusQuery = defineQuery<string>('getPaymentStatus');
export async function processPaymentWorkflow(data: PaymentData): Promise<any> {
let status = 'processing';
let transactionId: string | undefined;
// Set up signal and query handlers
setHandler(cancelPaymentSignal, (reason: string) => {
status = 'cancelled';
});
setHandler(getPaymentStatusQuery, () => status);
try {
// Execute payment activity
const result = await processPayment(data);
transactionId = result.transactionId;
status = 'completed';
return {
success: true,
transactionId,
status,
};
} catch (error) {
status = 'failed';
// Compensate if needed
if (transactionId) {
await refundPayment(transactionId);
}
throw error;
}
}
Inject TemporalService to start and manage workflows:
// payment.service.ts
import { Injectable } from '@nestjs/common';
import { TemporalService } from 'nestjs-temporal-core';
@Injectable()
export class PaymentService {
constructor(private readonly temporal: TemporalService) {}
async processPayment(paymentData: any) {
// Start workflow
const result = await this.temporal.startWorkflow(
'processPaymentWorkflow',
[paymentData],
{
workflowId: `payment-${Date.now()}`,
taskQueue: 'my-task-queue',
}
);
return {
workflowId: result.result.workflowId,
runId: result.result.runId,
};
}
async checkPaymentStatus(workflowId: string) {
// Query workflow
const statusResult = await this.temporal.queryWorkflow(
workflowId,
'getPaymentStatus'
);
return { status: statusResult.result };
}
async cancelPayment(workflowId: string, reason: string) {
// Send signal
await this.temporal.signalWorkflow(
workflowId,
'cancelPayment',
[reason]
);
}
}
The package provides modular architecture with separate modules for different use cases:
Complete integration with both client and worker capabilities:
import { TemporalModule } from 'nestjs-temporal-core';
TemporalModule.register({
connection: { address: 'localhost:7233' },
taskQueue: 'my-queue',
worker: {
workflowsPath: require.resolve('./workflows'),
activityClasses: [PaymentActivity, EmailActivity],
},
})
For services that only need to start/query workflows:
import { TemporalClientModule } from 'nestjs-temporal-core/client';
TemporalClientModule.register({
connection: { address: 'localhost:7233' },
namespace: 'default',
})
For dedicated worker processes:
import { TemporalWorkerModule } from 'nestjs-temporal-core/worker';
TemporalWorkerModule.register({
connection: { address: 'localhost:7233' },
taskQueue: 'worker-queue',
worker: {
workflowsPath: require.resolve('./workflows'),
activityClasses: [BackgroundActivity],
},
})
For standalone activity management:
import { TemporalActivityModule } from 'nestjs-temporal-core/activity';
TemporalActivityModule.register({
activityClasses: [DataProcessingActivity],
})
For managing Temporal schedules:
import { TemporalSchedulesModule } from 'nestjs-temporal-core/schedules';
TemporalSchedulesModule.register({
connection: { address: 'localhost:7233' },
})
TemporalModule.register({
connection: {
address: 'localhost:7233',
namespace: 'default',
},
taskQueue: 'my-task-queue',
worker: {
workflowsPath: require.resolve('./workflows'),
activityClasses: [PaymentActivity, EmailActivity],
autoStart: true,
maxConcurrentActivityExecutions: 100,
},
logLevel: 'info',
enableLogger: true,
})
New in 3.0.12: Support for multiple workers with different task queues in the same process.
TemporalModule.register({
connection: {
address: 'localhost:7233',
namespace: 'default',
},
autoRestart: true, // Global default for all workers
maxRestarts: 3, // Global default for all workers
workers: [
{
taskQueue: 'payments-queue',
workflowsPath: require.resolve('./workflows/payments'),
activityClasses: [PaymentActivity, RefundActivity],
autoStart: true,
maxRestarts: 5, // Override for this critical worker
workerOptions: {
maxConcurrentActivityTaskExecutions: 100,
},
},
{
taskQueue: 'notifications-queue',
workflowsPath: require.resolve('./workflows/notifications'),
activityClasses: [EmailActivity, SmsActivity],
autoStart: true,
workerOptions: {
maxConcurrentActivityTaskExecutions: 50,
},
},
{
taskQueue: 'background-jobs',
workflowsPath: require.resolve('./workflows/jobs'),
activityClasses: [DataProcessingActivity],
autoStart: false, // Start manually later
autoRestart: false, // Disable auto-restart for this worker
},
],
logLevel: 'info',
enableLogger: true,
})
import { Injectable } from '@nestjs/common';
import { TemporalService } from 'nestjs-temporal-core';
@Injectable()
export class WorkerManagementService {
constructor(private readonly temporal: TemporalService) {}
async checkWorkerStatus() {
// Get all workers info
const workersInfo = this.temporal.getAllWorkers();
console.log(`Total workers: ${workersInfo.totalWorkers}`);
console.log(`Running workers: ${workersInfo.runningWorkers}`);
// Get specific worker status
const paymentWorkerStatus = this.temporal.getWorkerStatusByTaskQueue('payments-queue');
if (paymentWorkerStatus?.isHealthy) {
console.log('Payment worker is healthy');
}
}
async controlWorkers() {
// Start a specific worker
await this.temporal.startWorkerByTaskQueue('background-jobs');
// Stop a specific worker
await this.temporal.stopWorkerByTaskQueue('notifications-queue');
}
async registerNewWorker() {
// Dynamically register a new worker at runtime
const result = await this.temporal.registerWorker({
taskQueue: 'new-queue',
workflowsPath: require.resolve('./workflows/new'),
activityClasses: [NewActivity],
autoStart: true,
});
if (result.success) {
console.log(`Worker registered for queue: ${result.taskQueue}`);
}
}
}
For users who need full control, you can access the native Temporal connection to create custom workers:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { TemporalService } from 'nestjs-temporal-core';
import { Worker } from '@temporalio/worker';
@Injectable()
export class CustomWorkerService implements OnModuleInit {
private customWorker: Worker;
constructor(private readonly temporal: TemporalService) {}
async onModuleInit() {
const workerManager = this.temporal.getWorkerManager();
const connection = workerManager.getConnection();
if (!connection) {
throw new Error('No connection available');
}
// Create your custom worker using the native Temporal SDK
this.customWorker = await Worker.create({
connection,
taskQueue: 'custom-task-queue',
namespace: 'default',
workflowsPath: require.resolve('./workflows/custom'),
activities: {
myCustomActivity: async (data: string) => {
return `Processed: ${data}`;
},
},
});
// Start the worker
await this.customWorker.run();
}
}
For dynamic configuration using environment variables or config services:
// config/temporal.config.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TemporalOptionsFactory, TemporalOptions } from 'nestjs-temporal-core';
@Injectable()
export class TemporalConfigService implements TemporalOptionsFactory {
constructor(private configService: ConfigService) {}
createTemporalOptions(): TemporalOptions {
return {
connection: {
address: this.configService.get('TEMPORAL_ADDRESS', 'localhost:7233'),
namespace: this.configService.get('TEMPORAL_NAMESPACE', 'default'),
},
taskQueue: this.configService.get('TEMPORAL_TASK_QUEUE', 'default'),
worker: {
workflowsPath: require.resolve('../workflows'),
activityClasses: [], // Populated by module
maxConcurrentActivityExecutions: 100,
},
};
}
}
// app.module.ts
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TemporalModule.registerAsync({
imports: [ConfigModule],
useClass: TemporalConfigService,
}),
],
})
export class AppModule {}
TemporalModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
connection: {
address: configService.get('TEMPORAL_ADDRESS', 'localhost:7233'),
namespace: configService.get('TEMPORAL_NAMESPACE', 'default'),
},
taskQueue: configService.get('TEMPORAL_TASK_QUEUE', 'default'),
worker: {
workflowsPath: require.resolve('./workflows'),
activityClasses: [PaymentActivity, EmailActivity],
},
}),
inject: [ConfigService],
})
For secure connections to Temporal Cloud:
import * as fs from 'fs';
TemporalModule.register({
connection: {
address: 'your-namespace.your-account.tmprl.cloud:7233',
namespace: 'your-namespace.your-account',
tls: {
clientCertPair: {
crt: fs.readFileSync('/path/to/client.crt'),
key: fs.readFileSync('/path/to/client.key'),
},
},
},
taskQueue: 'my-task-queue',
worker: {
workflowsPath: require.resolve('./workflows'),
activityClasses: [PaymentActivity],
},
})
interface TemporalOptions {
// Connection settings
connection: {
address: string; // Temporal server address (default: 'localhost:7233')
namespace?: string; // Temporal namespace (default: 'default')
tls?: TLSConfig; // TLS configuration for secure connections
};
// Task queue name
taskQueue?: string; // Default task queue (default: 'default')
// Worker configuration
worker?: {
workflowsPath?: string; // Path to workflow definitions (use require.resolve)
activityClasses?: any[]; // Array of activity classes to register
autoStart?: boolean; // Auto-start worker on module init (default: true)
autoRestart?: boolean; // Auto-restart on failure (inherits from global)
maxRestarts?: number; // Max restart attempts (inherits from global)
maxConcurrentActivityExecutions?: number; // Max concurrent activities (default: 100)
maxActivitiesPerSecond?: number; // Rate limit for activities
};
// Logging
logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error'; // Log level (default: 'info')
enableLogger?: boolean; // Enable logging (default: true)
// Auto-restart configuration
autoRestart?: boolean; // Auto-restart worker on failure (default: true)
maxRestarts?: number; // Max restart attempts before giving up (default: 3)
// Advanced
isGlobal?: boolean; // Make module global (default: false)
}
Activities are NestJS services decorated with @Activity() that perform actual work. They have full access to NestJS dependency injection and can interact with external systems.
Key Points:
@Injectable())@Activity() decorator at class level@ActivityMethod() decorator for methods to be registered@Injectable()
@Activity({ name: 'order-activities' })
export class OrderActivity {
constructor(
private readonly orderRepository: OrderRepository,
private readonly emailService: EmailService,
) {}
@ActivityMethod('createOrder')
async createOrder(orderData: CreateOrderData): Promise<Order> {
// Database operations with full DI support
const order = await this.orderRepository.create(orderData);
await this.emailService.sendConfirmation(order);
return order;
}
@ActivityMethod('validateInventory')
async validateInventory(items: OrderItem[]): Promise<boolean> {
// Business logic with injected services
return await this.orderRepository.checkInventory(items);
}
}
Workflows are pure Temporal functions (NOT NestJS services) that orchestrate activities. They must be deterministic and use Temporal's workflow APIs.
Important: Workflows are NOT decorated with @Injectable() and should NOT use NestJS dependency injection.
// order.workflow.ts
import { proxyActivities, defineSignal, defineQuery, setHandler } from '@temporalio/workflow';
import type { OrderActivity } from './order.activity';
// Create activity proxies with proper typing
const { createOrder, validateInventory } = proxyActivities<typeof OrderActivity.prototype>({
startToCloseTimeout: '5m',
retry: {
maximumAttempts: 3,
initialInterval: '1s',
maximumInterval: '30s',
},
});
// Define signals and queries at module level
export const cancelOrderSignal = defineSignal<[string]>('cancelOrder');
export const getOrderStatusQuery = defineQuery<string>('getOrderStatus');
// Workflow function (exported, not a class)
export async function processOrderWorkflow(orderData: CreateOrderData): Promise<OrderResult> {
let status = 'pending';
// Set up signal handler
setHandler(cancelOrderSignal, (reason: string) => {
status = 'cancelled';
});
// Set up query handler
setHandler(getOrderStatusQuery, () => status);
try {
// Validate inventory
const isValid = await validateInventory(orderData.items);
if (!isValid) {
throw new Error('Insufficient inventory');
}
// Create order
status = 'processing';
const order = await createOrder(orderData);
status = 'completed';
return {
orderId: order.id,
status,
};
} catch (error) {
status = 'failed';
throw error;
}
}
Signals allow external systems to send events to workflows, while queries provide read-only access to workflow state.
import { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow';
// Define at module level
export const updateStatusSignal = defineSignal<[string]>('updateStatus');
export const addItemSignal = defineSignal<[Item]>('addItem');
export const getItemsQuery = defineQuery<Item[]>('getItems');
export const getStatusQuery = defineQuery<string>('getStatus');
export async function myWorkflow(): Promise<void> {
let status = 'pending';
const items: Item[] = [];
// Set up handlers
setHandler(updateStatusSignal, (newStatus: string) => {
status = newStatus;
});
setHandler(addItemSignal, (item: Item) => {
items.push(item);
});
setHandler(getItemsQuery, () => items);
setHandler(getStatusQuery, () => status);
// Wait for completion signal
await condition(() => status === 'completed');
}
Updates combine the strengths of Signals (can mutate workflow state) and Queries (can return a result) into a single request/response operation. Handlers are declared with @UpdateMethod, exactly like @SignalMethod/@QueryMethod — metadata-only, consumed by the client side.
import { defineUpdate, setHandler } from '@temporalio/workflow';
export const depositUpdate = defineUpdate<number, [number]>('deposit');
export async function accountWorkflow(initialBalance: number): Promise<void> {
let balance = initialBalance;
setHandler(depositUpdate, (amount: number) => {
balance += amount;
return balance;
});
await condition(() => false); // keep running
}
Call it from a service via TemporalClientService:
// Wait for the update to complete and get its result
const newBalance = await this.clientService.updateWorkflow<number>(
'account-123',
'deposit',
[100],
);
// Or start it and only wait for it to be accepted, then await the result later
const updateHandle = await this.clientService.startUpdateWorkflow<number>(
'account-123',
'deposit',
[100],
);
const result = await updateHandle.result();
The typed workflow proxy exposes the same operations with full type inference:
const newBalance = await this.accountProxy.update('account-123', depositUpdate, 100);
Inject TemporalService in your NestJS services to interact with workflows:
@Injectable()
export class OrderService {
constructor(private readonly temporal: TemporalService) {}
async createOrder(orderData: CreateOrderData) {
// Start workflow - note the method signature
const result = await this.temporal.startWorkflow(
'processOrderWorkflow', // Workflow function name
[orderData], // Arguments array
{ // Options
workflowId: `order-${Date.now()}`,
taskQueue: 'order-queue',
}
);
return {
workflowId: result.result.workflowId,
runId: result.result.runId,
};
}
async queryOrderStatus(workflowId: string) {
const result = await this.temporal.queryWorkflow(
workflowId,
'getOrderStatus'
);
return result.result;
}
async cancelOrder(workflowId: string, reason: string) {
await this.temporal.signalWorkflow(
workflowId,
'cancelOrder',
[reason]
);
}
}
The typed workflow proxy gives you end-to-end type safety when interacting with a specific workflow. Instead of passing workflow names and args as strings/unknown[], you get a generic IWorkflowProxy<T> where T is your workflow function type — all method signatures are inferred from T.
What it solves:
// Before: string names, unknown args, manual casts on query results
const handle = await this.temporal.startWorkflow('orderWorkflow', [orderId, customerId]);
const status = await this.temporal.queryWorkflow<OrderStatus>(workflowId, 'getStatus');
// After: fully typed against the workflow signature
const handle = await this.orderProxy.start([orderId, customerId]); // args typed as Parameters<typeof orderWorkflow>
const status = await this.orderProxy.query(workflowId, statusQuery); // return type inferred from QueryDefinition
If you rename a workflow parameter or change its return type, every call site becomes a compile error until fixed.
// workflows/order.workflow.ts
import { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow';
export interface OrderStatus {
orderId: string;
state: 'pending' | 'approved' | 'shipped' | 'cancelled';
}
export const approveSignal = defineSignal<[string]>('approve'); // signal takes one string arg
export const cancelSignal = defineSignal<[string]>('cancel');
export const statusQuery = defineQuery<OrderStatus>('getStatus'); // query returns OrderStatus
export async function orderWorkflow(orderId: string, customerId: number): Promise<OrderStatus> {
let status: OrderStatus = { orderId, state: 'pending' };
setHandler(approveSignal, (approver) => {
status = { ...status, state: 'approved' };
});
setHandler(cancelSignal, (reason) => {
status = { ...status, state: 'cancelled' };
});
setHandler(statusQuery, () => status);
await condition(() => status.state !== 'pending');
return status;
}
// order.module.ts
import { Module } from '@nestjs/common';
import { createWorkflowToken, createWorkflowProvider } from 'nestjs-temporal-core';
import { orderWorkflow } from './workflows/order.workflow';
import { OrderService } from './order.service';
export const ORDER_WORKFLOW = createWorkflowToken('orderWorkflow');
@Module({
providers: [
OrderService,
createWorkflowProvider<typeof orderWorkflow>(ORDER_WORKFLOW, {
workflowType: 'orderWorkflow',
taskQueue: 'orders',
}),
],
exports: [ORDER_WORKFLOW],
})
export class OrderModule {}
// order.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { IWorkflowProxy } from 'nestjs-temporal-core';
import {
orderWorkflow,
approveSignal,
cancelSignal,
statusQuery,
OrderStatus,
} from './workflows/order.workflow';
import { ORDER_WORKFLOW } from './order.module';
@Injectable()
export class OrderService {
constructor(
@Inject(ORDER_WORKFLOW)
private readonly orderProxy: IWorkflowProxy<typeof orderWorkflow>,
) {}
async createOrder(orderId: string, customerId: number) {
// start() args are typed as Parameters<typeof orderWorkflow> = [string, number]
const handle = await this.orderProxy.start([orderId, customerId], {
workflowId: `order-${orderId}`,
});
return { workflowId: handle.workflowId };
}
async approve(workflowId: string, approver: string) {
// signal() infers TArgs from approveSignal — passing a number here is a compile error
await this.orderProxy.signal(workflowId, approveSignal, approver);
}
async getStatus(workflowId: string): Promise<OrderStatus> {
// query() return type is inferred from statusQuery
return this.orderProxy.query(workflowId, statusQuery);
}
async waitForCompletion(workflowId: string): Promise<OrderStatus> {
const handle = await this.orderProxy.getHandle(workflowId);
// handle.result() is Promise<OrderStatus>, not Promise<unknown>
return handle.result();
}
async cancelOrder(orderId: string, reason: string) {
// signalWithStart: atomically starts the workflow and signals it
await this.orderProxy.signalWithStart(
cancelSignal,
[reason], // signal args — typed
[orderId, 0], // workflow args — typed as Parameters<typeof orderWorkflow>
{ workflowId: `order-${orderId}` },
);
}
}
WorkflowProxyFactory directlyIf you don't want a token-bound provider, inject the factory and create proxies on demand:
import { Injectable } from '@nestjs/common';
import { WorkflowProxyFactory, IWorkflowProxy } from 'nestjs-temporal-core';
import { orderWorkflow } from './workflows/order.workflow';
@Injectable()
export class OrderService {
private readonly orderProxy: IWorkflowProxy<typeof orderWorkflow>;
constructor(factory: WorkflowProxyFactory) {
this.orderProxy = factory.createProxy<typeof orderWorkflow>({
workflowType: 'orderWorkflow',
taskQueue: 'orders',
});
}
}
WorkflowProxyFactory is registered globally by TemporalModule, so no imports are needed in feature modules.
| Method | Purpose | Typing |
|---|---|---|
start(args, options?) |
Start a new workflow execution | args typed as Parameters<T>; returns WorkflowHandleWithMetadata<T> |
getHandle(workflowId, runId?) |
Get a handle to an existing execution | Returns WorkflowHandle<T>; result() returns Promise<WorkflowResultType<T>> |
signal(workflowId, signalDef, ...args) |
Send a typed signal | args typed from SignalDefinition<TArgs> |
signalByName(workflowId, signalName, args?) |
Send a signal by string name | Fallback when no SignalDefinition is available |
query(workflowId, queryDef, ...args) |
Query with a typed definition | Return type inferred from QueryDefinition<TResult, TArgs> |
queryByName<TResult>(workflowId, queryName, args?) |
Query by string name | Caller specifies TResult |
signalWithStart(signalDef, signalArgs, workflowArgs, options?) |
Atomic start + signal | Both arg lists fully typed |
signalWithStart atomically starts a workflow and sends it a signal in one operation. If the workflow is already running, only the signal is delivered — no duplicate start, no race condition.
Use it for idempotent "ensure running + signal" patterns, e.g. a cart that should be started on the first item-add and signaled on every subsequent one.
// in workflows/cart.workflow.ts:
// export const addItemSignal = defineSignal<[CartItem]>('addItem');
// export async function cartWorkflow(userId: string) { ... }
await this.cartProxy.signalWithStart(
addItemSignal,
[{ sku: 'SKU-123', qty: 2 }], // signal args — typed from SignalDefinition
[userId], // workflow args — typed as Parameters<typeof cartWorkflow>
{ workflowId: `cart-${userId}`, taskQueue: 'carts' },
);
TemporalService (structured result)const result = await this.temporal.signalWithStart(
'cartWorkflow',
'addItem',
[{ sku: 'SKU-123', qty: 2 }],
[userId],
{ workflowId: `cart-${userId}`, taskQueue: 'carts' },
);
if (result.success) {
this.logger.log(`Signal '${result.signalName}' delivered to ${result.workflowId}`);
}
TemporalClientService (raw handle)const handle = await this.clientService.signalWithStart(
'orderWorkflow',
'approve',
['manager-approval'],
[orderId, customerId],
{
workflowId: `order-${orderId}`,
taskQueue: 'orders',
workflowIdReusePolicy: 'ALLOW_DUPLICATE',
workflowExecutionTimeout: '1h',
memo: { source: 'api' },
},
);
For detailed API documentation, visit the Full API Documentation.
The main unified service providing access to all Temporal functionality. See the API Documentation for complete method signatures and examples.
Key methods:
startWorkflow() - Start a workflow executionsignalWorkflow() - Send a signal to a running workflowsignalWithStart() - Atomically start a workflow and send it a signal (see Signal-with-Start)queryWorkflow() - Query a running workflowgetWorkflowHandle() - Get a workflow handle to interact with itterminateWorkflow() - Terminate a workflow executioncancelWorkflow() - Cancel a workflow executiongetHealth() - Get service health statuscreateSchedule() - Create a scheduleupsertSchedule() - Create a schedule, or update it in place if one with the same ID already existslistSchedules() - List all schedulesdeleteSchedule() - Delete a scheduleCreates typed IWorkflowProxy<T> instances. Registered globally by TemporalModule. See Typed Workflow Proxy for the full pattern.
createProxy<T>(config) - Create a typed proxy for a workflow function type TcreateWorkflowToken(workflowType) - Generate a unique NestJS injection token for a workflow proxycreateWorkflowProvider<T>(token, config) - Build a FactoryProvider that resolves to IWorkflowProxy<T>Check out our complete example repository featuring:
For more examples, visit our documentation. Key example scenarios include:
This section documents capabilities added in the Temporal SDK 1.16–1.19 line. Some require no code changes on your part — they pass straight through existing configuration options — others are new client methods.
Worker-side interceptors already work today via workerOptions.interceptors — it's spread wholesale into Worker.create(), so any interceptor accepted by @temporalio/worker just works:
TemporalModule.register({
connection: { address: 'localhost:7233' },
worker: {
workflowsPath: './dist/workflows',
workerOptions: {
interceptors: {
activityInbound: [(ctx) => new MyActivityInterceptor(ctx)],
},
},
},
});
Client-side interceptors (for the workflow/activity/schedule clients) are threaded via connection.interceptors:
TemporalModule.register({
connection: {
address: 'localhost:7233',
interceptors: {
workflow: [myWorkflowClientInterceptor],
},
},
});
Temporal SDK 1.19 enables gRPC gzip compression by default for the worker's native connection. If your server can't decompress it, opt out with { codec: 'none' }:
TemporalModule.register({
connection: {
address: 'localhost:7233',
grpcCompression: { codec: 'none' },
},
});
This only affects the worker's NativeConnection — the plain gRPC client connection (used for starting/signaling/querying workflows) does not compress by default and has no matching toggle.
Worker Deployments / Worker Versioning (GA in SDK 1.16+) also passes straight through workerOptions:
worker: {
workflowsPath: './dist/workflows',
workerOptions: {
workerDeploymentOptions: {
version: { buildId: 'v1.2.0', deploymentName: 'my-service' },
useWorkerVersioning: true,
},
},
},
For Activities that complete outside their handler (e.g. a human-in-the-loop approval, or a callback from another process), use the task-token-based completion methods on TemporalClientService:
await this.clientService.heartbeatActivity(taskToken, { progress: 50 });
await this.clientService.completeActivity(taskToken, { approved: true });
// or, on failure:
await this.clientService.failActivity(taskToken, new Error('rejected by approver'));
Public Preview — this is a Temporal server feature still in Public Preview; the underlying API may change in future SDK releases.
Standalone Activities run a durable, retryable Activity directly from the client — no workflow required:
const result = await this.clientService.executeStandaloneActivity('sendEmail', {
id: 'email-123',
taskQueue: 'emails',
args: ['user@example.com'],
startToCloseTimeout: '1m',
});
// Or start it and await the result later
const handle = await this.clientService.startStandaloneActivity('sendEmail', {
id: 'email-124',
taskQueue: 'emails',
startToCloseTimeout: '1m',
});
const outcome = await handle.result();
// Query executions
const info = await this.clientService.countStandaloneActivities('ActivityType="sendEmail"');
TemporalScheduleService covers the full schedule lifecycle, not just create/get:
await this.scheduleService.pauseSchedule('daily-report', 'investigating an incident');
await this.scheduleService.unpauseSchedule('daily-report');
await this.scheduleService.triggerSchedule('daily-report', 'ALLOW_ALL');
await this.scheduleService.deleteSchedule('daily-report');
await this.scheduleService.updateSchedule('daily-report', (previous) => ({
...previous,
spec: { ...previous.spec, cronExpressions: ['0 9 * * *'] },
}));
const { description } = await this.scheduleService.describeSchedule('daily-report');
const { schedules } = this.scheduleService.listSchedules();
for await (const schedule of schedules ?? []) {
console.log(schedule.scheduleId, schedule.info.numActions);
}
If you register schedules on every application bootstrap, createSchedule() throws ScheduleAlreadyRunning on the second and subsequent runs. Use upsertSchedule() instead — it creates the schedule if it doesn't exist yet, or updates it in place (spec, action, policies, state) if it does:
const result = await this.scheduleService.upsertSchedule({
scheduleId: 'daily-report',
spec: { cronExpressions: ['0 9 * * *'] },
action: {
type: 'startWorkflow',
workflowType: 'sendDailyReport',
taskQueue: 'reports',
args: [],
},
});
console.log(result.action); // 'created' | 'updated'
Explicitly out of scope (evaluated and deliberately not implemented): Nexus (standalone operations/service clients — a large, cross-namespace service-mesh feature outside a single-app NestJS wrapper's mission), the contrib packages (@temporalio/openai-agents, @temporalio/lambda-worker, @temporalio/workflow-streams, @temporalio/langsmith), and SerializationContext custom payload conversion (would require a much larger payload-converter extension point this wrapper doesn't expose today). Workflow-code-only concerns (named random streams, continue-as-new backoff interval, unsafe.random, workflow-failure-exception-type selection) are consumed directly via @temporalio/workflow in your workflow functions and aren't mediated by this package.
// workflow.ts
const paymentActivities = proxyActivities<typeof PaymentActivity.prototype>({
startToCloseTimeout: '5m',
retry: {
maximumAttempts: 5,
initialInterval: '1s',
maximumInterval: '1m',
backoffCoefficient: 2,
nonRetryableErrorTypes: ['InvalidPaymentMethod', 'InsufficientFunds'],
},
});
import { TestWorkflowEnvironment } from '@temporalio/testing';
import { Worker } from '@temporalio/worker';
import { processOrderWorkflow } from './order.workflow';
describe('Order Workflow', () => {
let testEnv: TestWorkflowEnvironment;
beforeAll(async () => {
testEnv = await TestWorkflowEnvironment.createTimeSkipping();
});
afterAll(async () => {
await testEnv?.teardown();
});
it('should process order successfully', async () => {
const { client, nativeConnection } = testEnv;
// Mock activities
const mockOrderActivity = {
validatePayment: async () => ({ valid: true }),
reserveInventory: async () => ({ reservationId: 'res-123' }),
chargePayment: async () => ({ transactionId: 'txn-123' }),
sendConfirmationEmail: async () => {},
};
const worker = await Worker.create({
connection: nativeConnection,
taskQueue: 'test',
workflowsPath: require.resolve('./order.workflow'),
activities: mockOrderActivity,
});
await worker.runUntil(async () => {
const result = await client.workflow.execute(processOrderWorkflow, {
workflowId: 'test-order-1',
taskQueue: 'test',
args: [{
orderId: 'order-123',
payment: { amount: 100, currency: 'USD' },
items: [{ id: '1', quantity: 1 }],
}],
});
expect(result.status).toBe('completed');
expect(result.transactionId).toBe('txn-123');
});
});
});
✅ DO:
defineSignal and defineQuery at module level❌ DON'T:
@Injectable() on workflow functionsMath.random() or Date.now() directly in workflows✅ DO:
@Injectable() and leverage NestJS DI@Activity() and @ActivityMethod() decorators❌ DON'T:
✅ DO:
❌ DON'T:
✅ DO:
❌ DON'T:
✅ DO:
❌ DON'T:
The package includes comprehensive health monitoring capabilities for production deployments.
// app.module.ts
import { Module } from '@nestjs/common';
import { TemporalModule } from 'nestjs-temporal-core';
import { TemporalHealthModule } from 'nestjs-temporal-core/health';
@Module({
imports: [
TemporalModule.register({
connection: { address: 'localhost:7233' },
taskQueue: 'my-queue',
worker: {
workflowsPath: require.resolve('./workflows'),
activityClasses: [MyActivity],
},
}),
TemporalHealthModule, // Adds /health/temporal endpoint
],
})
export class AppModule {}
@Controller('health')
export class HealthController {
constructor(private readonly temporal: TemporalService) {}
@Get('/status')
async getHealthStatus() {
const health = this.temporal.getHealth();
return {
status: health.overallHealth,
timestamp: new Date(),
services: {
client: {
healthy: health.client.status === 'healthy',
connection: health.client.connectionStatus,
},
worker: {
healthy: health.worker.status === 'healthy',
state: health.worker.state,
activitiesRegistered: health.worker.activitiesCount,
},
discovery: {
healthy: health.discovery.status === 'healthy',
activitiesDiscovered: health.discovery.activitiesDiscovered,
},
},
uptime: health.uptime,
};
}
}
Problem: Cannot connect to Temporal server
Solutions:
// Check connection configuration
const health = temporalService.getHealth();
console.log('Connection status:', health.client.connectionStatus);
// Verify Temporal server is running
// docker ps | grep temporal
// Check connection settings
TemporalModule.register({
connection: {
address: process.env.TEMPORAL_ADDRESS || 'localhost:7233',
namespace: 'default',
},
})
Problem: Workflow cannot find registered activities
Solutions:
// 1. Ensure activity is in activityClasses array
TemporalModule.register({
worker: {
activityClasses: [MyActivity], // Must include the activity class
},
})
// 2. Verify activity is registered as provider
@Module({
providers: [MyActivity], // Must be in providers array
})
// 3. Check activity decorator
@Activity({ name: 'my-activities' })
export class MyActivity {
@ActivityMethod('myActivity')
async myActivity() { }
}
// 4. Check discovery status
const health = temporalService.getHealth();
console.log('Activities discovered:', health.discovery.activitiesDiscovered);
Problem: Workflow not found or not executing
Solutions:
// 1. Ensure workflowsPath is correct
TemporalModule.register({
worker: {
workflowsPath: require.resolve('./workflows'), // Must resolve to workflows file/directory
},
})
// 2. Export workflow function properly
// workflows/index.ts
export { processOrderWorkflow } from './order.workflow';
export { reportWorkflow } from './report.workflow';
// 3. Use correct workflow name when starting
await temporal.startWorkflow(
'processOrderWorkflow', // Must match exported function name
[args],
options
);
Problem: Activities or workflows timing out
Solutions:
// Configure appropriate timeouts
const activities = proxyActivities<typeof MyActivity.prototype>({
startToCloseTimeout: '10m', // Increase for long-running activities
scheduleToCloseTimeout: '15m', // Total time including queuing
scheduleToStartTimeout: '5m', // Time waiting in queue
});
// For workflows
await temporal.startWorkflow('myWorkflow', [args], {
workflowExecutionTimeout: '24h', // Max total execution time
workflowRunTimeout: '12h', // Max single run time
workflowTaskTimeout: '10s', // Decision task timeout
});
Enable comprehensive debugging:
TemporalModule.register({
logLevel: 'debug',
enableLogger: true,
connection: {
address: 'localhost:7233',
},
worker: {
debugMode: true, // If available
},
})
// Check detailed health and statistics
const health = temporalService.getHealth();
const stats = temporalService.getStatistics();
console.log('Health:', JSON.stringify(health, null, 2));
console.log('Stats:', JSON.stringify(stats, null, 2));
If you're still experiencing issues:
getHealth() to identify failing componentsVersion 3.0.12 introduces support for multiple workers without breaking existing single-worker configurations.
Your existing configuration continues to work:
// ✅ This still works exactly as before
TemporalModule.register({
connection: { address: 'localhost:7233' },
taskQueue: 'my-queue',
worker: {
workflowsPath: require.resolve('./workflows'),
activityClasses: [MyActivity],
},
})
After (v3.0.12):
// Option 1: Configure multiple workers in module
TemporalModule.register({
connection: { address: 'localhost:7233' },
workers: [
{
taskQueue: 'main-queue',
workflowsPath: require.resolve('./workflows/main'),
activityClasses: [MainActivity],
},
{
taskQueue: 'schedule-queue',
workflowsPath: require.resolve('./workflows/schedule'),
activityClasses: [ScheduleActivity],
},
],
})
// Get native connection for custom worker creation
const workerManager = temporal.getWorkerManager();
const connection: NativeConnection | null = workerManager.getConnection();
// Get specific worker by task queue
const worker: Worker | null = temporal.getWorker('payments-queue');
// Get all workers information
const workersInfo: MultipleWorkersInfo = temporal.getAllWorkers();
console.log(`${workersInfo.runningWorkers}/${workersInfo.totalWorkers} workers running`);
// Control specific workers
await temporal.startWorkerByTaskQueue('payments-queue');
await temporal.stopWorkerByTaskQueue('notifications-queue');
// Register new worker dynamically
const result = await temporal.registerWorker({
taskQueue: 'new-queue',
workflowsPath: require.resolve('./workflows/new'),
activityClasses: [NewActivity],
autoStart: true,
});
This release is backward-compatible — existing code continues to compile and run. The headline additions are a typed workflow proxy, signalWithStart on both service layers, and correctness fixes for a few schedule fields that were previously silently ignored.
Every type that was previously exported is still exported with the same name. Old field shapes continue to compile:
spec.timezones?: string[] on ScheduleSpec (deprecated — prefer SDK timezone singular, automatically normalized at runtime)action.retryPolicy? on schedule actions (deprecated — prefer SDK retry, automatically forwarded)searchAttributes?: Record<string, unknown> on ScheduleCreationOptionsenableSDKTracing? / enableOpenTelemetry? on WorkerCreateOptions (deprecated no-ops — had no effect in prior versions either)Three schedule fields were previously declared in the API but silently dropped by the SDK because of wrong field names or wrong shapes. They now work as the field name promises:
| Field | Before v3.3.0 | After v3.3.0 |
|---|---|---|
spec.timezone on a schedule |
Written to spec.timeZone (wrong casing) — SDK ignored it, schedules always ran in UTC |
Routed to SDK's timezone — schedule honors the zone |
description on createSchedule() |
Passed as a top-level field SDK ignored | Flows to state.note |
searchAttributes on createSchedule() |
Cast to typedSearchAttributes with the wrong shape |
Routed to the correct searchAttributes SDK field |
Action: if you had set timezone on a @Scheduled or createSchedule() call and configured your schedule times assuming UTC (because the timezone was being ignored), double-check your schedule timing after upgrade — the timezone will now actually apply.
limitedActions on createSchedule() remains a no-op for backward compatibility; set state.remainingActions directly via the SDK if you need that behavior.
import {
IWorkflowProxy,
WorkflowProxyFactory,
createWorkflowToken,
createWorkflowProvider,
} from 'nestjs-temporal-core';
// Typed proxy — see "Typed Workflow Proxy" section for the full pattern
const ORDER_WORKFLOW = createWorkflowToken('orderWorkflow');
const provider = createWorkflowProvider<typeof orderWorkflow>(ORDER_WORKFLOW, {
workflowType: 'orderWorkflow',
taskQueue: 'orders',
});
// Atomic start + signal — see "Signal-with-Start" section
await temporal.signalWithStart(
'cartWorkflow',
'addItem',
[{ sku: 'SKU-123', qty: 2 }],
[userId],
{ workflowId: `cart-${userId}`, taskQueue: 'carts' },
);
@temporalio/* 1.19 upgrade (Workflow Updates, Standalone Activities, Schedule lifecycle)This release bumps the peer dependency range to @temporalio/* ^1.15.0 || ^1.19.0 and adds four feature areas: Workflow Update support, Async Activity Completion, Standalone Activities, and full schedule lifecycle management. See Advanced Configuration for usage of all new APIs.
Temporal SDK 1.19 requires Node.js >= 20.3.0. engines.node has been updated from >=16.0.0 accordingly. If you're on Node 16 or 18, either upgrade Node or pin @temporalio/* to ^1.15.0 in your own package.json (this package's peer range still allows it) and stay on this package's previous minor version.
workerOptions.workerDeploymentOptions) and worker-side interceptors (workerOptions.interceptors) already passed through wholesale to Worker.create() — they work automatically now that the SDK types include them.connection.grpcCompression = { codec: 'none' }.import { UpdateMethod } from 'nestjs-temporal-core';
// Workflow Update handler (mirrors @SignalMethod/@QueryMethod)
@UpdateMethod('deposit')
async handleDeposit(amount: number): Promise<number> { /* ... */ }
// Client-side Update calls
await clientService.updateWorkflow<number>('account-123', 'deposit', [100]);
await clientService.startUpdateWorkflow<number>('account-123', 'deposit', [100]);
// Async Activity Completion
await clientService.completeActivity(taskToken, result);
await clientService.failActivity(taskToken, error);
// Standalone Activities (Public Preview)
await clientService.executeStandaloneActivity('sendEmail', { id: 'e-1', taskQueue: 'q' });
// Schedule lifecycle
await scheduleService.pauseSchedule('daily-report');
await scheduleService.triggerSchedule('daily-report');
await scheduleService.deleteSchedule('daily-report');
@temporalio/* 1.19; if you're on Node 16/18, stay on nestjs-temporal-core@<version> pinned to @temporalio/* ^1.15.0)We welcome contributions! To contribute:
git checkout -b feature/amazing-feature)npm test)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)# Clone the repository
git clone https://github.com/harsh-simform/nestjs-temporal-core.git
cd nestjs-temporal-core
# Install dependencies
npm install
# Run tests
npm test
# Run tests with coverage
npm run test:cov
# Build the package
npm run build
# Generate documentation
npm run docs:generate
This project is licensed under the MIT License - see the LICENSE file for details.
⭐ Star us on GitHub if you find this project helpful!
Made with ❤️ by the Harsh Simform