Skip to content

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.

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

typescript
taskQueue: "orders";

Global Activities

Activities that are available to all workflows in the contract:

typescript
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 parameters
  • output: Zod schema for workflow return value
  • activities: Workflow-specific activities (optional)
typescript
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:

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

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

typescript
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 defaultOptionsactivityOptionsByName (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):

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

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

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

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

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

typescript
// ✅ 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

typescript
// ✅ Good - clear and descriptive
workflows: {
  processOrderPayment: { /* ... */ },
  cancelOrderAndRefund: { /* ... */ },
}

// ❌ Avoid - vague names
workflows: {
  process: { /* ... */ },
  handle: { /* ... */ },
}

3. Document Complex Schemas

typescript
/**
 * 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

Released under the MIT License.