Defining Contracts
Learn how to define type-safe contracts for your Temporal workflows and activities.
Overview
Contracts are the foundation of temporal-contract. They define the interface for your workflows, including inputs, outputs, and activities, all using Standard Schema compatible libraries (Zod, Valibot, ArkType) for validation.
Basic Contract Structure
Composition-first: define resources individually, then compose them into the contract. Named resources are reusable across workflows and contracts, get precise hover/jump-to-definition, and keep defineContract a readable table of contents instead of a wall of inline literals.
import { defineActivity, defineContract, defineWorkflow } from "@temporal-contract/contract";
import { z } from "zod";
// Define resources first...
const log = defineActivity({
input: z.object({
level: z.enum(["info", "warn", "error"]),
message: z.string(),
}),
output: z.void(),
});
const processPayment = defineActivity({
input: z.object({
userId: z.string(),
amount: z.number(),
}),
output: z.object({
transactionId: z.string(),
}),
});
const myWorkflow = defineWorkflow({
input: z.object({
userId: z.string(),
amount: z.number().positive(),
}),
output: z.object({
success: z.boolean(),
transactionId: z.string().optional(),
}),
// Workflow-specific activities
activities: { processPayment },
});
// ...then compose the contract from references.
export const myContract = defineContract({
taskQueue: "my-task-queue",
// Global activities available to all workflows
activities: { log },
workflows: { myWorkflow },
});Contract Elements
Task Queue
The task queue name that workers will listen on:
taskQueue: "orders";Global Activities
Activities that are available to all workflows in the contract:
activities: {
sendEmail: {
input: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string()
}),
output: z.object({ sent: z.boolean() }),
},
}Workflows
Each workflow must define:
input: Zod schema for workflow parametersoutput: Zod schema for workflow return valueactivities: Workflow-specific activities (optional)
workflows: {
processOrder: {
input: z.object({ orderId: z.string() }),
output: z.object({ status: z.string() }),
activities: { /* ... */ }
}
}Workflow-Specific Activities
Activities that are only available within a specific workflow:
workflows: {
processOrder: {
// ...
activities: {
chargeCard: {
input: z.object({ amount: z.number() }),
output: z.object({ success: z.boolean() }),
},
},
},
}Typed Errors
Activities and workflows can declare their domain failures on the contract. The error name becomes the ApplicationFailure.type on the wire, data is a Standard Schema for the structured payload (validated on both sides of the boundary), and nonRetryable drives Temporal's retry policy from the contract — so retry semantics ship with the contract instead of being scattered across workers:
workflows: {
processOrder: {
input: OrderSchema,
output: OrderResultSchema,
errors: {
EmptyOrder: {
data: z.object({ orderId: z.string() }),
nonRetryable: true,
},
},
activities: {
processPayment: {
input: z.object({ amount: z.number() }),
output: PaymentResultSchema,
errors: {
PaymentDeclined: {
data: z.object({ reason: z.string() }),
message: "The payment was declined",
nonRetryable: true, // permanent — Temporal stops retrying
},
GatewayUnavailable: {}, // data-less, retryable
},
},
},
},
}Declared errors surface as typed, schema-validated ContractError values: on the workflow side when calling the activity, and on the client side when awaiting the workflow result. See Activity Handlers for producing them and Client Usage for consuming them.
Default Activity Options
An activity can carry its default ActivityOptions (timeouts, retry policy) on the contract, making operational behavior part of the shared source of truth:
activities: {
sendNotification: {
input: NotificationSchema,
output: z.void(),
defaultOptions: {
startToCloseTimeout: "30 seconds",
retry: { maximumAttempts: 5 },
},
},
}Merge precedence at the worker, least → most specific: declareWorkflow's activityOptions (workflow-wide default) → the activity's contract-level defaultOptions → activityOptionsByName (explicit per-workflow override). Deployment-specific routing (taskQueue) stays worker-side in activityOptionsByName.
Schema Validation
All inputs and outputs are validated using Standard Schema compatible libraries (e.g., Zod):
input: z.object({
email: z.string().email(), // Email validation
age: z.number().int().positive(), // Integer validation
status: z.enum(["active", "inactive"]), // Enum validation
metadata: z.record(z.string()).optional(), // Optional fields
});Type Inference
TypeScript automatically infers types from your schemas:
const contract = defineContract({
taskQueue: "orders",
workflows: {
processOrder: {
input: z.object({
orderId: z.string(),
amount: z.number(),
}),
output: z.object({
success: z.boolean(),
}),
activities: {},
},
},
});
// Types are automatically inferred:
// input: { orderId: string; amount: number }
// output: { success: boolean }Complex Schemas
Use Zod's full power for complex validations:
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/),
});
const OrderSchema = z.object({
items: z
.array(
z.object({
productId: z.string(),
quantity: z.number().int().positive(),
}),
)
.min(1),
shippingAddress: AddressSchema,
billingAddress: AddressSchema.optional(),
total: z.number().positive(),
});
export const orderContract = defineContract({
taskQueue: "orders",
workflows: {
processOrder: {
input: OrderSchema,
output: z.object({
orderId: z.string(),
status: z.enum(["pending", "completed", "failed"]),
}),
activities: {},
},
},
});Reusable Schemas
Define schemas once and reuse them:
const PaymentInput = z.object({
amount: z.number().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
});
const PaymentOutput = z.object({
transactionId: z.string(),
status: z.enum(["success", "failed"]),
});
export const contract = defineContract({
taskQueue: "payments",
workflows: {
processPayment: {
input: PaymentInput,
output: PaymentOutput,
activities: {
chargeCard: {
input: PaymentInput,
output: PaymentOutput,
},
},
},
},
});Multiple Workflows
A contract can define multiple workflows:
export const ecommerceContract = defineContract({
taskQueue: "ecommerce",
workflows: {
processOrder: {
input: z.object({ orderId: z.string() }),
output: z.object({ success: z.boolean() }),
activities: {
/* ... */
},
},
processRefund: {
input: z.object({ orderId: z.string(), reason: z.string() }),
output: z.object({ refunded: z.boolean() }),
activities: {
/* ... */
},
},
updateInventory: {
input: z.object({ productId: z.string(), delta: z.number() }),
output: z.object({ newQuantity: z.number() }),
activities: {
/* ... */
},
},
},
});Best Practices
1. Keep Contracts Focused
Group related workflows in the same contract:
// ✅ Good - related workflows together
export const orderContract = defineContract({
taskQueue: "orders",
workflows: {
createOrder: {
/* ... */
},
cancelOrder: {
/* ... */
},
updateOrder: {
/* ... */
},
},
});
// ❌ Avoid - mixing unrelated workflows
export const contract = defineContract({
taskQueue: "everything",
workflows: {
processOrder: {
/* ... */
},
sendEmail: {
/* ... */
},
generateReport: {
/* ... */
},
},
});2. Use Descriptive Names
// ✅ Good - clear and descriptive
workflows: {
processOrderPayment: { /* ... */ },
cancelOrderAndRefund: { /* ... */ },
}
// ❌ Avoid - vague names
workflows: {
process: { /* ... */ },
handle: { /* ... */ },
}3. Document Complex Schemas
/**
* Order processing workflow
*
* Handles the complete order lifecycle including:
* - Payment processing
* - Inventory reservation
* - Shipping coordination
*/
processOrder: {
input: z.object({
orderId: z.string().describe('Unique order identifier'),
customerId: z.string().describe('Customer UUID'),
items: z.array(OrderItemSchema).describe('List of items to purchase'),
}),
// ...
}See Also
- Client Usage - Using contracts with the client
- Worker Usage - Implementing contracts in workers
- Core Concepts - Understanding the contract-first approach