Skip to content

temporal-contractType-safe contracts for Temporal.io

End-to-end type safety · Runtime validation · Explicit error handling

temporal-contracttemporal-contract

Define once, use everywhere

typescript
import { defineActivity, defineContract, defineWorkflow } from "@temporal-contract/contract";
import { z } from "zod";

const chargeCard = defineActivity({
  input: z.object({ customerId: z.string(), amount: z.number().positive() }),
  output: z.object({ transactionId: z.string() }),
});

const processOrder = defineWorkflow({
  input: z.object({
    orderId: z.string(),
    customerId: z.string(),
    amount: z.number().positive(),
  }),
  output: z.object({ orderId: z.string(), transactionId: z.string() }),
  // Payment already moved money on success — block a second successful
  // run per order. A start is still retryable after a genuinely failed
  // attempt (e.g. a declined payment, where no charge went through).
  idempotency: "retry-if-failed",
  activities: { chargeCard },
});

export const orderContract = defineContract({
  taskQueue: "orders",
  workflows: { processOrder },
});
typescript
import { declareActivitiesHandler, qualifyFailure } from "@temporal-contract/worker/activity";
import { fromPromise } from "unthrown";

import { orderContract } from "./contract.js";
import { gateway, GatewayError } from "./services.js";

export const activities = declareActivitiesHandler({
  contract: orderContract,
  activities: {
    // Workflow-scoped activities nest under their workflow, mirroring the contract.
    processOrder: {
      chargeCard: ({ customerId, amount }) =>
        fromPromise(
          gateway.charge(customerId, amount),
          // Anticipated failures become a typed ApplicationFailure; anything
          // else (a bug) stays a loud defect.
          qualifyFailure("CHARGE_FAILED", { expected: GatewayError }),
        ).map((charge) => ({
          transactionId: charge.id,
        })),
    },
  },
});
typescript
import { declareWorkflow, propagateActivityFailure } from "@temporal-contract/worker/workflow";

import { orderContract } from "./contract.js";

export const processOrder = declareWorkflow({
  workflowName: "processOrder",
  contract: orderContract,
  activityOptions: { startToCloseTimeout: "1 minute", retry: { maximumAttempts: 3 } },
  implementation: async (context, order) => {
    // `order` is typed from the contract. So is the return value. Every
    // activity call returns an AsyncResult; `propagateActivityFailure` lets
    // Temporal's retry policy decide the outcome.
    const { transactionId } = await propagateActivityFailure(
      context.activities.chargeCard({
        customerId: order.customerId,
        amount: order.amount,
      }),
    );

    return { orderId: order.orderId, transactionId };
  },
});
typescript
import {
  tagPatterns,
  TypedClient,
  WORKFLOW_RESULT_ERROR_TAGS,
  WORKFLOW_START_ERROR_TAGS,
} from "@temporal-contract/client";
import { Client, Connection } from "@temporalio/client";

import { orderContract } from "./contract.js";

const connection = await Connection.connect({ address: "localhost:7233" });

const client = await TypedClient.create({
  client: new Client({ connection }),
}).get();

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

result.match({
  ok: (output) => console.log(output.transactionId), // ✅ typed
  errCases: (matcher) =>
    matcher.with(
      ...tagPatterns(WORKFLOW_START_ERROR_TAGS),
      ...tagPatterns(WORKFLOW_RESULT_ERROR_TAGS),
      (error) => console.error("failed:", error.message),
    ),
  defect: (cause) => console.error("unexpected:", cause),
});

Where to start

You want to…Go to
Build something end to endYour first workflow
Solve a specific problemHow-to guides
Look up an option or typeReference
Understand why it works this wayExplanation
Upgrade from 7.xUpgrade to v8

The documentation follows the Diátaxis framework: tutorials teach, how-to guides solve, reference describes, explanation clarifies.

Released under the MIT License.