Introducing temporal-contract
January 2025
We're excited to introduce temporal-contract, a library that brings end-to-end type safety and contract-first development to Temporal.io workflows.
The Challenge
Temporal.io is an excellent platform for building durable, fault-tolerant applications. However, working with workflows in TypeScript presents some challenges:
- No compile-time type safety between client and worker
- Manual validation of workflow inputs and outputs
- Scattered type definitions across different files
- Runtime errors from mismatched data types
The Solution
temporal-contract solves these problems with a contract-first approach:
import { defineContract } from "@temporal-contract/contract";
import { z } from "zod";
export const orderContract = defineContract({
taskQueue: "orders",
workflows: {
processOrder: {
input: z.object({
orderId: z.string(),
customerId: z.string(),
amount: z.number().positive(),
}),
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() }),
},
},
},
},
});From this single contract definition, you get:
- Full TypeScript inference - No manual type annotations needed
- Automatic validation - Zod validates inputs and outputs at runtime
- IDE support - Autocomplete, inline docs, and refactoring
- Compile-time checks - TypeScript catches errors before runtime
Type-Safe Client
import { TypedClient } from "@temporal-contract/client";
const client = TypedClient.create(orderContract, temporalClient);
// TypeScript knows the exact shape of args and result
const future = client.executeWorkflow("processOrder", {
workflowId: "order-123",
args: {
orderId: "ORD-123",
customerId: "CUST-456",
amount: 99.99,
},
});
const result = await future;
result.match({
Ok: (output) => console.log(output.status), // 'success' | 'failed'
Error: (error) => console.error("Failed:", error),
});Type-Safe Worker
import { declareWorkflow } from "@temporal-contract/worker/workflow";
const processOrder = declareWorkflow({
workflowName: "processOrder",
contract: orderContract,
implementation: async ({ activities }, { orderId, customerId, amount }) => {
// Activities are fully typed
const { transactionId } = await activities.processPayment({
customerId,
amount,
});
return { status: "success", transactionId };
},
});Key Features
Schema Validation Support
temporal-contract works with multiple schema libraries:
NestJS Integration
First-class NestJS support with dependency injection:
import { TemporalClientModule } from "@temporal-contract/client-nestjs";
@Module({
imports: [
TemporalClientModule.forRoot({
connection: { address: "localhost:7233" },
}),
],
})
export class AppModule {}Result Pattern
Explicit error handling with the Result/Future pattern:
const result = await client.executeWorkflow("processOrder", options);
result.match({
Ok: (output) => {
// Handle success
},
Error: (error) => {
// Handle failure explicitly
},
});Getting Started
Install the packages:
pnpm add @temporal-contract/contract @temporal-contract/client @temporal-contract/worker zodThen follow our Getting Started Guide to build your first type-safe workflow.
Inspired By
temporal-contract brings the contract-first patterns from these excellent projects to Temporal.io:
- tRPC - End-to-end type safety for RPC
- oRPC - Contract-first RPC with OpenAPI
- ts-rest - Type-safe REST APIs
What's Next
We're actively developing temporal-contract with planned features including:
- Nexus integration for cross-namespace workflows
- OpenTelemetry support for observability
- More schema library support
Join Us
- Star us on GitHub
- Report issues or request features
- Contribute to the project
We'd love to hear your feedback and see what you build with temporal-contract!