Skip to content

temporal-contractType-safe contracts for Temporal.io

End-to-end type safety and automatic validation for workflows and activities

temporal-contract

The Problem โ€‹

Working with Temporal.io workflows is powerful, but comes with challenges:

typescript
// โŒ No type safety
const result = await client.workflow.execute('processOrder', {
  workflowId: 'order-123',
  taskQueue: 'orders',
  args: [{ orderId: 'ORD-123' }],  // What fields? What types?
});

console.log(result.status);  // unknown type, no autocomplete

// โŒ Manual validation everywhere
// โŒ Runtime errors from wrong data
// โŒ Scattered activity definitions

How It Works โ€‹

The Solution โ€‹

temporal-contract transforms your Temporal workflows with a contract-first approach:

typescript
// โœ… Define once
const contract = defineContract({
  taskQueue: 'orders',
  workflows: {
    processOrder: {
      input: z.object({ orderId: z.string(), customerId: z.string() }),
      output: z.object({ status: z.enum(['success', 'failed']), transactionId: z.string() }),
      activities: { /* ... */ }
    }
  }
});

// โœ… Type-safe client
const result = await client.executeWorkflow('processOrder', {
  workflowId: 'order-123',
  args: { orderId: 'ORD-123', customerId: 'CUST-456' },  // TypeScript knows!
});

console.log(result.status);  // 'success' | 'failed' โ€” full autocomplete!

Quick Example โ€‹

See how easy it is to get started with a complete workflow:

typescript
import { defineContract } from '@temporal-contract/contract';
import { z } from 'zod';

// โœ… Define your contract once
export const orderContract = defineContract({
  taskQueue: 'orders',
  workflows: {
    processOrder: {
      input: z.object({
        orderId: z.string(),
        customerId: z.string(),
        amount: z.number()
      }),
      output: z.object({
        status: z.enum(['success', 'failed']),
        transactionId: z.string().optional()
      }),
      activities: {
        processPayment: {
          input: z.object({
            customerId: z.string(),
            amount: z.number()
          }),
          output: z.object({ transactionId: z.string() }),
        },
        sendNotification: {
          input: z.object({
            customerId: z.string(),
            message: z.string()
          }),
          output: z.void(),
        },
      },
    },
  },
});
typescript
import { Future, Result } from '@temporal-contract/boxed';
import { declareActivitiesHandler, ActivityError } from '@temporal-contract/worker/activity';
import { orderContract } from './contract';

// โœ… Implement activities with full type safety
// Note: Replace paymentService and notificationService with your actual service implementations
export const activities = declareActivitiesHandler({
  contract: orderContract,
  activities: {
    processOrder: {
      processPayment: ({ customerId, amount }) => {
        // Replace paymentService with your actual payment service
        return Future.fromPromise(
          paymentService.charge(customerId, amount)
        ).mapOk((transaction) => ({ transactionId: transaction.id }))
          .mapError((error) =>
            new ActivityError(
              'PAYMENT_FAILED',
              error instanceof Error ? error.message : 'Payment processing failed',
              error
            )
          );
      },

      sendNotification: ({ customerId, message }) => {
        // Replace notificationService with your actual notification service
        return Future.fromPromise(
          notificationService.send(customerId, message)
        ).mapError((error) =>
          new ActivityError(
            'NOTIFICATION_FAILED',
            error instanceof Error ? error.message : 'Failed to send notification',
            error
          )
        );
      },
    },
  },
});
typescript
import { declareWorkflow } from '@temporal-contract/worker/workflow';
import { orderContract } from './contract';

// โœ… Type-safe workflow orchestration
export const processOrder = declareWorkflow({
  workflowName: 'processOrder',
  contract: orderContract,
  implementation: async (context, { orderId, customerId, amount }) => {
    // Activities are fully typed!
    const { transactionId } = await context.activities.processPayment({
      customerId,
      amount,
    });

    await context.activities.sendNotification({
      customerId,
      message: `Order ${orderId} confirmed!`,
    });

    return { status: 'success', transactionId };
  },
});
typescript
import { NativeConnection } from '@temporalio/worker';
import { createWorker } from '@temporal-contract/worker/worker';
import { orderContract } from './contract';
import { activities } from './activities';

// โœ… Start the worker
const connection = await NativeConnection.connect({
  address: 'localhost:7233',
});

const worker = await createWorker({
  contract: orderContract,
  connection,
  namespace: 'default',
  workflowsPath: './workflows',
  activities,
});

await worker.run(); // Worker is now listening!
typescript
import { TypedClient } from '@temporal-contract/client';
import { orderContract } from './contract';

// โœ… Type-safe client calls
const client = TypedClient.create(orderContract, { connection });

const result = await client.executeWorkflow('processOrder', {
  workflowId: 'order-123',
  args: {
    orderId: 'ORD-123',
    customerId: 'CUST-456',
    amount: 99.99,
  },
});

// Full autocomplete on result!
console.log(result.status); // 'success' | 'failed'
console.log(result.transactionId); // string | undefined

What You Get โ€‹

With temporal-contract, you get a complete, type-safe workflow system:

  1. Contract Definition - Define your workflow interface once with Zod schemas
  2. Activity Implementation - Implement business logic with full type safety
  3. Workflow Orchestration - Coordinate activities with typed context
  4. Worker Setup - Register activities and workflows automatically
  5. Type-safe Client - Call workflows with autocomplete and validation

All parts work together seamlessly with end-to-end type safety!

Released under the MIT License.