Getting Started
Welcome to temporal-contract! This guide will help you get up and running with type-safe Temporal workflows.
What is temporal-contract?
temporal-contract is a TypeScript library that brings end-to-end type safety and automatic validation to Temporal.io workflows and activities. It uses a contract-first approach with Zod schemas to ensure your workflows are type-safe from definition to execution.
Why Use temporal-contract?
The Problem
Working with Temporal.io is powerful, but comes with challenges:
- No type safety — Workflow parameters and return types are loosely typed
- Manual validation — You need to validate inputs and outputs manually
- Runtime errors — Type mismatches are only caught at runtime
- Scattered definitions — Activity types are defined separately from workflows
The Solution
temporal-contract solves these problems by:
- ✅ End-to-end type safety — From contract to client, workflows, and activities
- ✅ Automatic validation — Zod schemas validate at all network boundaries
- ✅ Compile-time checks — TypeScript catches issues before runtime
- ✅ Better DX — Autocomplete, refactoring support, inline documentation
Installation
Install the required packages:
bash
pnpm add @temporal-contract/contract @temporal-contract/worker @temporal-contract/client
pnpm add zod @temporalio/client @temporalio/worker @temporalio/workflowbash
npm install @temporal-contract/contract @temporal-contract/worker @temporal-contract/client
npm install zod @temporalio/client @temporalio/worker @temporalio/workflowbash
yarn add @temporal-contract/contract @temporal-contract/worker @temporal-contract/client
yarn add zod @temporalio/client @temporalio/worker @temporalio/workflowQuick Start
Let's build a simple order processing workflow in 3 steps.
1. Define Your Contract
Create a contract that defines your workflow's interface:
typescript
// contract.ts
import { defineContract } from "@temporal-contract/contract";
import { z } from "zod";
export const orderContract = defineContract({
taskQueue: "orders",
// Global activities available to all workflows
activities: {
sendEmail: {
input: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
}),
output: z.object({ sent: z.boolean() }),
},
},
workflows: {
processOrder: {
input: z.object({
orderId: z.string(),
customerId: z.string(),
}),
output: z.object({
status: z.enum(["success", "failed"]),
transactionId: z.string(),
}),
// Workflow-specific activities
activities: {
processPayment: {
input: z.object({
customerId: z.string(),
amount: z.number().positive(),
}),
output: z.object({
transactionId: z.string(),
success: z.boolean(),
}),
},
},
},
},
});2. Implement Activities & Workflows
Implement your activities and workflows with full type safety:
typescript
// activities.ts
import { declareActivitiesHandler, ActivityError } from "@temporal-contract/worker/activity";
import { Future, Result } from "@swan-io/boxed";
import { orderContract } from "./contract";
export const activities = declareActivitiesHandler({
contract: orderContract,
activities: {
sendEmail: async ({ to, subject, body }) => {
// Full type safety - parameters are automatically typed!
return Future.fromPromise(emailService.send({ to, subject, body }))
.mapError(
(error) =>
new ActivityError(
"EMAIL_FAILED",
error instanceof Error ? error.message : "Failed to send email",
error,
),
)
.mapOk(() => ({ sent: true }));
},
processPayment: async ({ customerId, amount }) => {
// TypeScript knows the exact types
return Future.fromPromise(paymentGateway.charge(customerId, amount))
.mapError(
(error) =>
new ActivityError(
"PAYMENT_FAILED",
error instanceof Error ? error.message : "Payment failed",
error,
),
)
.mapOk((txId) => ({ transactionId: txId, success: true }));
},
},
});typescript
// workflows.ts
import { declareWorkflow } from "@temporal-contract/worker/workflow";
import { orderContract } from "./contract";
export const processOrder = declareWorkflow({
workflowName: "processOrder",
contract: orderContract,
implementation: async ({ activities }, { orderId, customerId }) => {
// Full autocomplete for activities and their parameters
// Activities return plain values (Result is unwrapped internally)
const payment = await activities.processPayment({
customerId,
amount: 100,
});
await activities.sendEmail({
to: customerId,
subject: "Order Confirmed",
body: `Order ${orderId} processed`,
});
// Return plain object (not Result - network serialization requirement)
return {
status: payment.success ? "success" : "failed",
transactionId: payment.transactionId,
};
},
});3. Start Worker & Call from Client
Set up your worker and client:
typescript
// worker.ts
import { Worker } from "@temporalio/worker";
import { activities } from "./activities";
const worker = await Worker.create({
workflowsPath: require.resolve("./workflows"),
activities,
taskQueue: "orders", // or activities.contract.taskQueue
});
await worker.run();typescript
// client.ts
import { TypedClient } from "@temporal-contract/client";
import { Connection, Client } from "@temporalio/client";
import { orderContract } from "./contract";
const connection = await Connection.connect({
address: "localhost:7233",
});
const temporalClient = new Client({ connection });
const client = TypedClient.create(orderContract, temporalClient);
// Fully typed workflow execution with Result/Future pattern
const future = client.executeWorkflow("processOrder", {
workflowId: "order-123",
args: { orderId: "ORD-123", customerId: "CUST-456" },
});
const result = await future;
result.match({
Ok: (output) => {
console.log(output.status); // 'success' | 'failed' — fully typed!
},
Error: (error) => {
console.error("Workflow failed:", error);
},
});What's Next?
- 📚 Learn about Core Concepts
- 🔨 Explore Worker Implementation
- 📖 Check out Examples
- 🔍 Browse the API Reference