Type-safe contracts for Temporal.io
End-to-end type safety and runtime validation for workflows and activities
Temporal invokes workflows by string name with positional arguments:
await client.workflow.execute("processOrder", {
taskQueue: "orders",
workflowId: "order-123",
args: [{ orderId: "ORD-1", amount: 99.99 }],
});Nothing here is checked — not the name, not the queue, not the argument shape.
The client and the worker are usually different deployments on different release
cadences, so typeof activities is a claim nobody verifies. Rename a field and
the workflow reads undefined, runs to completion, and does the wrong thing —
durably.
Declare the shape once; both sides import it.
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() }),
activities: { chargeCard },
});
export const orderContract = defineContract({
taskQueue: "orders",
workflows: { processOrder },
});Implement the activities — note that workflow-scoped activities nest under their workflow, mirroring the contract:
import { declareActivitiesHandler, qualify } from "@temporal-contract/worker/activity";
import { fromPromise } from "unthrown";
export const activities = declareActivitiesHandler({
contract: orderContract,
activities: {
processOrder: {
chargeCard: ({ customerId, amount }) =>
fromPromise(gateway.charge(customerId, amount), qualify("CHARGE_FAILED")).map((charge) => ({
transactionId: charge.id,
})),
},
},
});Call it — names, arguments, and results all typed, and validated at runtime:
const result = await client.executeWorkflow("processOrder", {
workflowId: "order-123",
args: { orderId: "ORD-1", customerId: "CUST-1", amount: 99.99 },
});
result.match({
ok: (output) => console.log(output.transactionId),
errCases: (matcher) =>
matcher.with(
tag("@temporal-contract/WorkflowValidationError"),
tag("@temporal-contract/WorkflowFailedError"),
// ...exhaustive — a missing tag is a compile error
(error) => console.error(error.message),
),
defect: (cause) => console.error("unexpected:", cause),
});An invalid call is rejected before a workflow is started — no history, no partial state, nothing to unwind.
- End-to-end type safety — workflows, activities, signals, queries, updates, errors, and search attributes all derive from one contract
- Validation at every boundary — Standard Schema (Zod, Valibot, ArkType) runs on both sides of every network hop
- Typed domain errors — declare failures on the contract; consume them as schema-validated values with an exhaustive matcher
- Explicit error handling —
Result/AsyncResultfrom unthrown, with a separatedefectchannel that keeps genuine bugs loud - Child workflows — typed, including across contracts and teams
- Schedules, cancellation scopes, continue-as-new, activity middleware, client interceptors — all contract-aware
- Testing utilities — time-skipping (no Docker) and real-server (testcontainers) fixtures
- Nexus — not implemented; see the status page
# Core packages
pnpm add @temporal-contract/contract @temporal-contract/worker @temporal-contract/client
# Peer dependencies
pnpm add unthrown zod \
@temporalio/client @temporalio/common @temporalio/worker @temporalio/workflowRequires Node.js ≥ 22.19, ESM ("type": "module"), and TypeScript strict.
Developed against TypeScript 6.0.
Install
unthrownexplicitly even if your package manager auto-installs peers — your own code imports itsResult/AsyncResulttypes, so it is a real dependency of yours. It must resolve to v5.
See Install for the per-process breakdown.
📖 btravstack.github.io/temporal-contract
Organized by Diátaxis:
| Tutorial | Build a working app end to end |
| How-to guides | Recipes for specific problems |
| Reference | Every option, type, and error |
| Explanation | Why it works this way |
| Package | Description |
|---|---|
| @temporal-contract/contract | Contract builders, types, and errors |
| @temporal-contract/worker | Workflow, activity, and worker entry points |
| @temporal-contract/client | Typed client, handles, and schedules |
| @temporal-contract/testing | Time-skipping and testcontainers fixtures |
All four version together as a fixed release group — one version number describes a compatible set.
The contract API (defineContract, declareWorkflow, declareActivitiesHandler,
TypedClient) is stable. Earlier major bumps were migrations of the underlying
result library, now settled on unthrown;
there are no plans to switch again. The unthrown peer range tracks its current
major line, and each raise is documented in the changelog with migration notes.
Upgrading from 7.x? See Upgrade to v8.
See CONTRIBUTING.md.
MIT