# NestJS Temporal Core

<div align="center">

A comprehensive NestJS integration framework for Temporal.io that provides enterprise-ready workflow orchestration with automatic discovery, declarative decorators, and robust monitoring capabilities.

![Statements](https://img.shields.io/badge/statements-99.62%25-brightgreen.svg?style=flat)
![Branches](https://img.shields.io/badge/branches-94.26%25-brightgreen.svg?style=flat)
![Functions](https://img.shields.io/badge/functions-98.01%25-brightgreen.svg?style=flat)
![Lines](https://img.shields.io/badge/lines-99.75%25-brightgreen.svg?style=flat)
[![codecov](https://codecov.io/gh/harsh-simform/nestjs-temporal-core/branch/main/graph/badge.svg?token=BYSE45L6DI)](https://codecov.io/gh/harsh-simform/nestjs-temporal-core)

[Documentation](https://harsh-simform.github.io/nestjs-temporal-core/) • [NPM](https://www.npmjs.com/package/nestjs-temporal-core) • [GitHub](https://github.com/harsh-simform/nestjs-temporal-core) • [Example Project](https://github.com/harsh-simform/nestjs-temporal-core-example)

</div>

---

## Table of Contents

- [Overview](#overview)
- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Module Variants](#module-variants)
- [Configuration](#configuration)
  - [Basic Configuration](#basic-configuration)
  - [Multiple Workers](#multiple-workers-configuration)
  - [Async Configuration](#async-configuration)
  - [TLS Configuration](#tls-configuration-temporal-cloud)
- [Core Concepts](#core-concepts)
  - [Activities](#activities)
  - [Workflows](#workflows)
  - [Signals and Queries](#signals-and-queries)
  - [Typed Workflow Proxy](#typed-workflow-proxy)
  - [Signal-with-Start](#signal-with-start)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Advanced Usage](#advanced-usage)
- [Best Practices](#best-practices)
- [Health Monitoring](#health-monitoring)
- [Troubleshooting](#troubleshooting)
- [Migration Guide](#migration-guide)
- [Contributing](#contributing)
- [License](#license)

## Overview

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.

### Why NestJS Temporal Core?

| 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 |

## Features

### Core Capabilities

- **Declarative Decorators** - Use `@Activity()` and `@ActivityMethod()` for clean, intuitive activity definitions
- **Automatic Discovery** - Runtime discovery and registration of activities with zero configuration
- **Schedule Management** - Programmatic schedule creation, updates, and monitoring
- **Health Monitoring** - Built-in health checks and comprehensive status reporting
- **Typed Workflow Proxy** - Generic `IWorkflowProxy<T>` that infers start args, signal args, and query return types from your workflow function signature
- **Signal-with-Start** - Atomic "ensure running + signal" on both the low-level client service and the high-level `TemporalService`

### Enterprise Features

- **Connection Management** - Automatic connection pooling and lifecycle management
- **Error Handling** - Structured error handling with detailed logging and retry policies
- **Performance Monitoring** - Built-in metrics, statistics, and performance tracking
- **Graceful Shutdown** - Clean resource cleanup and connection termination

### Flexibility & Scalability

- **Modular Design** - Use only what you need (client-only, worker-only, or combined)
- **Multiple Workers** - Support for multiple workers with different task queues
- **Advanced Configuration** - Extensive customization for production environments
- **TLS Support** - Secure connections for Temporal Cloud deployments

[🔝 Back to top](#table-of-contents)

## Installation

```bash
npm install nestjs-temporal-core @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/common
```

### Peer Dependencies

```bash
npm install @nestjs/common @nestjs/core reflect-metadata rxjs
```

[🔝 Back to top](#table-of-contents)

## Quick Start

### 1. Enable Shutdown Hooks

Enable shutdown hooks in your `main.ts` for proper Temporal resource cleanup:

```typescript
// 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();
```

### 2. Configure the Module

Import and configure `TemporalModule` in your app module:

```typescript
// 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 {}
```

### 3. Define Activities

Create activities using `@Activity()` and `@ActivityMethod()` decorators:

```typescript
// 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()}` };
  }
}
```

### 4. Define Workflows

Create workflows as pure Temporal functions (NOT NestJS services):

```typescript
// 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;
  }
}
```

### 5. Use in Services

Inject `TemporalService` to start and manage workflows:

```typescript
// 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]
    );
  }
}
```

[🔝 Back to top](#table-of-contents)

## Module Variants

The package provides modular architecture with separate modules for different use cases:

### 1. Unified Module (Recommended)

Complete integration with both client and worker capabilities:

```typescript
import { TemporalModule } from 'nestjs-temporal-core';

TemporalModule.register({
  connection: { address: 'localhost:7233' },
  taskQueue: 'my-queue',
  worker: {
    workflowsPath: require.resolve('./workflows'),
    activityClasses: [PaymentActivity, EmailActivity],
  },
})
```

### 2. Client-Only Module

For services that only need to start/query workflows:

```typescript
import { TemporalClientModule } from 'nestjs-temporal-core/client';

TemporalClientModule.register({
  connection: { address: 'localhost:7233' },
  namespace: 'default',
})
```

### 3. Worker-Only Module

For dedicated worker processes:

```typescript
import { TemporalWorkerModule } from 'nestjs-temporal-core/worker';

TemporalWorkerModule.register({
  connection: { address: 'localhost:7233' },
  taskQueue: 'worker-queue',
  worker: {
    workflowsPath: require.resolve('./workflows'),
    activityClasses: [BackgroundActivity],
  },
})
```

### 4. Activity-Only Module

For standalone activity management:

```typescript
import { TemporalActivityModule } from 'nestjs-temporal-core/activity';

TemporalActivityModule.register({
  activityClasses: [DataProcessingActivity],
})
```

### 5. Schedules-Only Module

For managing Temporal schedules:

```typescript
import { TemporalSchedulesModule } from 'nestjs-temporal-core/schedules';

TemporalSchedulesModule.register({
  connection: { address: 'localhost:7233' },
})
```

[🔝 Back to top](#table-of-contents)

## Configuration

### Basic Configuration

```typescript
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,
})
```

### Multiple Workers Configuration

**New in 3.0.12**: Support for multiple workers with different task queues in the same process.

```typescript
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,
})
```

#### Accessing Multiple Workers

```typescript
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}`);
    }
  }
}
```

### Manual Worker Creation (Advanced)

For users who need full control, you can access the native Temporal connection to create custom workers:

```typescript
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();
  }
}
```

### Async Configuration

For dynamic configuration using environment variables or config services:

```typescript
// 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 {}
```

### Alternative Async Pattern (useFactory)

```typescript
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],
})
```

### TLS Configuration (Temporal Cloud)

For secure connections to Temporal Cloud:

```typescript
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],
  },
})
```

### Configuration Options Reference

```typescript
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)
}
```

[🔝 Back to top](#table-of-contents)

## Core Concepts

### Activities

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:**
- Activities are NestJS services (`@Injectable()`)
- Use `@Activity()` decorator at class level
- Use `@ActivityMethod()` decorator for methods to be registered
- Activities should be idempotent and handle retries gracefully
- Full access to NestJS DI (inject services, repositories, etc.)

```typescript
@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

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.

```typescript
// 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 and Queries

Signals allow external systems to send events to workflows, while queries provide read-only access to workflow state.

```typescript
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');
}
```

### Using Workflows in Services

Inject `TemporalService` in your NestJS services to interact with workflows:

```typescript
@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]
    );
  }
}
```

[🔝 Back to top](#table-of-contents)

### Typed Workflow Proxy

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:**

```typescript
// 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');
```

```typescript
// 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.

#### 1. Define signal/query definitions in your workflow file

```typescript
// 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;
}
```

#### 2. Register a typed proxy as a NestJS provider

```typescript
// 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 {}
```

#### 3. Inject and use — fully typed

```typescript
// 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}` },
    );
  }
}
```

#### Alternative: use `WorkflowProxyFactory` directly

If you don't want a token-bound provider, inject the factory and create proxies on demand:

```typescript
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.

#### Proxy method reference

| 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 |

[🔝 Back to top](#table-of-contents)

### Signal-with-Start

`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.

#### Via the typed proxy (recommended)

```typescript
// 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' },
);
```

#### Via `TemporalService` (structured result)

```typescript
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}`);
}
```

#### Via `TemporalClientService` (raw handle)

```typescript
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' },
  },
);
```

[🔝 Back to top](#table-of-contents)

## API Reference

For detailed API documentation, visit the [Full API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/).

### TemporalService

The main unified service providing access to all Temporal functionality. See the [API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/) for complete method signatures and examples.

Key methods:
- `startWorkflow()` - Start a workflow execution
- `signalWorkflow()` - Send a signal to a running workflow
- `signalWithStart()` - Atomically start a workflow and send it a signal (see [Signal-with-Start](#signal-with-start))
- `queryWorkflow()` - Query a running workflow
- `getWorkflowHandle()` - Get a workflow handle to interact with it
- `terminateWorkflow()` - Terminate a workflow execution
- `cancelWorkflow()` - Cancel a workflow execution
- `getHealth()` - Get service health status
- `createSchedule()` - Create a schedule
- `listSchedules()` - List all schedules
- `deleteSchedule()` - Delete a schedule

### WorkflowProxyFactory

Creates typed `IWorkflowProxy<T>` instances. Registered globally by `TemporalModule`. See [Typed Workflow Proxy](#typed-workflow-proxy) for the full pattern.

- `createProxy<T>(config)` - Create a typed proxy for a workflow function type `T`

### Proxy provider helpers

- `createWorkflowToken(workflowType)` - Generate a unique NestJS injection token for a workflow proxy
- `createWorkflowProvider<T>(token, config)` - Build a `FactoryProvider` that resolves to `IWorkflowProxy<T>`

[🔝 Back to top](#table-of-contents)

## Examples

### 🚀 Example Project

Check out our **[complete example repository](https://github.com/harsh-simform/nestjs-temporal-core-example)** featuring:

- ✅ **Real-world implementations** - Production-ready examples
- ✅ **Multiple use cases** - E-commerce, notifications, reports, and more
- ✅ **Best practices** - Following all recommended patterns
- ✅ **Docker setup** - Ready-to-run with docker-compose
- ✅ **Test coverage** - Comprehensive test examples

### 📚 Documentation Examples

For more examples, visit our [documentation](https://harsh-simform.github.io/nestjs-temporal-core/). Key example scenarios include:

1. **E-commerce Order Processing** - Complete example with compensation logic
2. **Scheduled Reports** - Creating and managing scheduled workflows
3. **Activity Retry Configuration** - Custom retry policies
4. **Child Workflows** - Organizing complex workflows
5. **Continue-As-New** - For long-running workflows
6. **Custom Error Handling** - Implementing custom error types

[🔝 Back to top](#table-of-contents)

## Advanced Usage

### Activity Retry Configuration

```typescript
// workflow.ts
const paymentActivities = proxyActivities<typeof PaymentActivity.prototype>({
  startToCloseTimeout: '5m',
  retry: {
    maximumAttempts: 5,
    initialInterval: '1s',
    maximumInterval: '1m',
    backoffCoefficient: 2,
    nonRetryableErrorTypes: ['InvalidPaymentMethod', 'InsufficientFunds'],
  },
});
```

### Workflow Testing

```typescript
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');
    });
  });
});
```

[🔝 Back to top](#table-of-contents)

## Best Practices

### 1. Workflow Design

**✅ DO:**
- Keep workflows deterministic (no random numbers, current time, network calls)
- Use activities for any non-deterministic operations
- Keep workflow history size manageable (use continue-as-new for long-running workflows)
- Export workflow functions (not classes)
- Use `defineSignal` and `defineQuery` at module level

**❌ DON'T:**
- Don't use `@Injectable()` on workflow functions
- Don't inject NestJS services in workflows
- Don't use `Math.random()` or `Date.now()` directly in workflows
- Don't make HTTP calls or database queries directly in workflows

### 2. Activity Design

**✅ DO:**
- Make activities idempotent (safe to retry)
- Use `@Injectable()` and leverage NestJS DI
- Use `@Activity()` and `@ActivityMethod()` decorators
- Handle errors appropriately
- Log activity execution for debugging

**❌ DON'T:**
- Don't make activities too granular (network overhead)
- Don't rely on activity execution order guarantees
- Don't share mutable state between activity invocations

### 3. Configuration

**✅ DO:**
- Use async configuration for environment-based setup
- Configure appropriate timeouts for your use case
- Set up proper retry policies
- Enable graceful shutdown hooks
- Use task queues to organize work

**❌ DON'T:**
- Don't hardcode connection strings
- Don't use the same task queue for all workflows
- Don't ignore timeout configurations

### 4. Error Handling

**✅ DO:**
- Implement compensation logic in workflows
- Use appropriate retry policies
- Log errors with context
- Define non-retryable error types
- Handle activity failures gracefully

**❌ DON'T:**
- Don't swallow errors silently
- Don't retry indefinitely
- Don't ignore business-level failures

### 5. Testing

**✅ DO:**
- Write unit tests for activities
- Use TestWorkflowEnvironment for integration tests
- Mock external dependencies
- Test failure scenarios
- Test signal and query handlers

**❌ DON'T:**
- Don't skip workflow testing
- Don't test against production Temporal server
- Don't assume workflows are correct without testing

[🔝 Back to top](#table-of-contents)

## Health Monitoring

The package includes comprehensive health monitoring capabilities for production deployments.

### Using Built-in Health Module

```typescript
// 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 {}
```

### Custom Health Checks

```typescript
@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,
    };
  }
}
```

[🔝 Back to top](#table-of-contents)

## Troubleshooting

### Common Issues and Solutions

#### 1. Connection Errors

**Problem:** Cannot connect to Temporal server

**Solutions:**
```typescript
// 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',
  },
})
```

#### 2. Activity Not Found

**Problem:** Workflow cannot find registered activities

**Solutions:**
```typescript
// 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);
```

#### 3. Workflow Registration Issues

**Problem:** Workflow not found or not executing

**Solutions:**
```typescript
// 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
);
```

#### 4. Timeout Issues

**Problem:** Activities or workflows timing out

**Solutions:**
```typescript
// 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
});
```

### Debug Mode

Enable comprehensive debugging:

```typescript
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));
```

### Getting Help

If you're still experiencing issues:

1. **Check the logs** - Enable debug logging to see detailed information
2. **Verify configuration** - Double-check all connection and worker settings
3. **Test connectivity** - Ensure Temporal server is accessible
4. **Review health status** - Use `getHealth()` to identify failing components
5. **Check GitHub Issues** - [Search existing issues](https://github.com/harsh-simform/nestjs-temporal-core/issues)
6. **Create an issue** - Provide logs, configuration, and minimal reproduction

[🔝 Back to top](#table-of-contents)

## Migration Guide

### Migrating to v3.0.12+ (Multiple Workers Support)

Version 3.0.12 introduces support for multiple workers without breaking existing single-worker configurations.

#### No Changes Required for Single Worker

Your existing configuration continues to work:

```typescript
// ✅ This still works exactly as before
TemporalModule.register({
  connection: { address: 'localhost:7233' },
  taskQueue: 'my-queue',
  worker: {
    workflowsPath: require.resolve('./workflows'),
    activityClasses: [MyActivity],
  },
})
```

#### Migrating to Multiple Workers

**After (v3.0.12):**
```typescript
// 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],
    },
  ],
})
```

#### New APIs in v3.0.12

```typescript
// 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,
});
```

### Migrating to v3.3.0 (Typed Workflow Proxy + Schedule fixes)

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.

#### No changes required

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 `ScheduleCreationOptions`
- `enableSDKTracing?` / `enableOpenTelemetry?` on `WorkerCreateOptions` (deprecated no-ops — had no effect in prior versions either)

#### Runtime behavior fixes

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.

#### New APIs in v3.3.0

```typescript
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' },
);
```

[🔝 Back to top](#table-of-contents)

## Requirements

- **Node.js**: >= 16.0.0
- **NestJS**: >= 9.0.0
- **Temporal Server**: >= 1.20.0

## Contributing

We welcome contributions! To contribute:

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Run tests (`npm test`)
5. Commit your changes (`git commit -m 'Add amazing feature'`)
6. Push to the branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request

### Development Setup

```bash
# 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
```

[🔝 Back to top](#table-of-contents)

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Support and Resources

- 📚 **Documentation**: [Full API Documentation](https://harsh-simform.github.io/nestjs-temporal-core/)
- 🐛 **Issues**: [GitHub Issues](https://github.com/harsh-simform/nestjs-temporal-core/issues)
- 💬 **Discussions**: [GitHub Discussions](https://github.com/harsh-simform/nestjs-temporal-core/discussions)
- 📦 **NPM**: [nestjs-temporal-core](https://www.npmjs.com/package/nestjs-temporal-core)
- 🔄 **Changelog**: [Releases](https://github.com/harsh-simform/nestjs-temporal-core/releases)
- 📖 **Example Project**: [nestjs-temporal-core-example](https://github.com/harsh-simform/nestjs-temporal-core-example)

## Related Projects

- [Temporal.io](https://temporal.io/) - The underlying workflow orchestration platform
- [NestJS](https://nestjs.com/) - Progressive Node.js framework
- [@temporalio/sdk](https://www.npmjs.com/package/@temporalio/client) - Official Temporal TypeScript SDK

---

<div align="center">

**[⭐ Star us on GitHub](https://github.com/harsh-simform/nestjs-temporal-core)** if you find this project helpful!

Made with ❤️ by the Harsh Simform

</div>
