Validated at every boundary
Validated on send, parsed on receive — every network hop is checked and transforms apply exactly once. A malformed call is rejected before a workflow is ever started — no history, no partial state.
End-to-end type safety · Runtime validation · Explicit error handling
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 },
});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,
})),
},
},
});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 };
},
});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),
});| You want to… | Go to |
|---|---|
| Build something end to end | Your first workflow |
| Solve a specific problem | How-to guides |
| Look up an option or type | Reference |
| Understand why it works this way | Explanation |
| Upgrade from 7.x | Upgrade to v8 |
The documentation follows the Diátaxis framework: tutorials teach, how-to guides solve, reference describes, explanation clarifies.