Skip to content

feat: typed contract errors, activity middleware + context, contract-level activity defaults#298

Open
btravers wants to merge 1 commit into
mainfrom
feat/orpc-inspired-contract-features
Open

feat: typed contract errors, activity middleware + context, contract-level activity defaults#298
btravers wants to merge 1 commit into
mainfrom
feat/orpc-inspired-contract-features

Conversation

@btravers

Copy link
Copy Markdown
Collaborator

Summary

Three features inspired by an analysis of oRPC v2's contract-first design, adapted to Temporal's failure and options model. Closes the gap where domain failures collapsed into an opaque ApplicationFailure / WorkflowFailedError.cause, and formalizes two patterns the docs previously showed as manual boilerplate (middleware, dependency injection).

1. Contract-declared typed domain errors

  • defineActivity / defineWorkflow accept an errors map ({ data?: StandardSchema, message?, nonRetryable? } per name), validated at defineContract time.
  • Activity implementations receive typed constructors via a new (optional) helpers argument: (args, { errors }) => Err(errors.PaymentDeclined({ reason })). The boundary validates the payload and throws ApplicationFailure with type = error name, details[0] = validated data, and nonRetryable driven by the contract — retry semantics ship with the contract instead of being scattered per worker. Undeclared names / invalid payloads fail fast with the new terminal ContractErrorDataValidationError.
  • Workflow side: activities with declared errors return AsyncResult<Output, DeclaredUnion | ActivityError | ActivityCancelledError>, exactly mirroring the child-workflow API (cancellation discriminated first, Temporal's ActivityFailure wrapper unwrapped). Activities without declared errors keep the throwing Promise shape — fully backward compatible.
  • Workflows declare their own errors; throw context.errors.EmptyOrder({...}) fails the execution as a typed ApplicationFailure, and the client rehydrates matching failures into ContractError on executeWorkflow / handle.result() (falling back to WorkflowFailedError on type/payload mismatch, so contract drift degrades to today's untyped behavior instead of a wrong typed error).
  • New @temporal-contract/contract/errors entry point (same pattern as ./result-async) keeps unthrown an optional peer of the root entry.

2. Activity middleware + typed dependency context

declareActivitiesHandler gains:

  • createContext — typed DI, invoked per execution with { activityName, workflowName }, surfaced to implementations and middleware as helpers.context.
  • middleware — contract-aware chain, outermost-first, running inside the validation boundary and operating on the unthrown AsyncResult (observes modeled failures on the err channel, can short-circuit, can substitute input).

3. Contract-level default activity options

defineActivity({ defaultOptions }) carries timeouts/retry policy on the contract (structural types — the contract package keeps zero @temporalio/* deps; strict-object validation catches typos at definition time). Worker merge precedence per activity: activityOptions (workflow-wide) < contract defaultOptions < activityOptionsByName. The one-extra-proxy-per-customized-activity optimization is preserved.

Testing

  • 317 unit tests pass (46 new across 5 new spec files + extensions to builder.spec, workflow-proxy.spec, client.spec)
  • Integration suite (Docker, real Temporal server) passes: 35 tests
  • pnpm build, typecheck, lint, knip, format --check all clean
  • Docs updated (defining-contracts, activity-handlers, worker-implementation, client-usage, .agents/rules/*) and a minor changeset added for all three packages

Related

Follow-up issues from the same analysis: #296 (contract introspection/diff tooling), #297 (@temporal-contract/experimental-effect).

Notes for review

  • Error data is validated on both producer and rehydration sides, matching the existing deliberate double-validation — same transform caveat as activity outputs.
  • ValidationError subclass rules respected: ContractErrorDataValidationError extends the ApplicationFailure-based ValidationError (terminal, deterministic).

🤖 Generated with Claude Code

…level activity defaults

Three features inspired by oRPC v2's contract-first design, adapted to
Temporal's failure and options model:

- Contract-declared typed domain errors: `errors` maps on
  defineActivity/defineWorkflow, typed constructors in activity helpers
  and workflow context, ApplicationFailure serialization (type = error
  name, details = validated data, nonRetryable from the contract), and
  typed rehydration on the workflow proxy and the client.
- Activity middleware + typed dependency context on
  declareActivitiesHandler (createContext + contract-aware middleware
  chain operating on the AsyncResult inside the validation boundary).
- Contract-level default ActivityOptions on defineActivity, merged as
  activityOptions < defaultOptions < activityOptionsByName.

New `@temporal-contract/contract/errors` entry point keeps unthrown an
optional peer of the root entry. Activities without declared errors keep
the throwing Promise shape — backward compatible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 11, 2026 01:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends temporal-contract to make contracts a stronger source of truth for failure semantics and operational behavior by adding typed, schema-validated domain errors, a contract-aware activity handler pipeline (middleware + DI context), and contract-level default activity options.

Changes:

  • Add contract-declared typed domain errors for activities and workflows, with worker-side serialization to ApplicationFailure and workflow/client-side rehydration into ContractError.
  • Add declareActivitiesHandler support for per-invocation typed context (createContext) and an outermost-first middleware chain that operates on AsyncResult.
  • Add contract-level per-activity defaultOptions and merge them into workflow activity proxies with documented precedence.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/worker/src/workflow.ts Adds workflow-level typed error constructors (context.errors) and converts thrown ContractError into ApplicationFailure.
packages/worker/src/internal.ts Merges contract defaultOptions into activity proxy options with precedence over workflow-wide defaults.
packages/worker/src/activity.ts Adds typed contract errors for activities, per-invocation context injection, and middleware support inside the validation boundary.
packages/worker/src/activities-proxy.ts Changes workflow-side activity calls to return AsyncResult for errors-declaring activities and rehydrates typed errors.
packages/worker/src/errors.ts Introduces ContractErrorDataValidationError plus ActivityError/ActivityCancelledError for classification.
packages/worker/src/contract-errors.ts Adds internal bridge converting ContractErrorApplicationFailure with schema validation and contract retry semantics.
packages/worker/src/workflow-proxy.spec.ts Adds tests for contract-level defaultOptions precedence/merging behavior.
packages/worker/src/workflow-errors.spec.ts Adds runtime coverage for workflow-level typed errors being converted to ApplicationFailure.
packages/worker/src/activity-contract-errors.spec.ts Adds runtime coverage for activity typed errors, middleware behavior, and createContext.
packages/worker/src/activities-proxy.spec.ts Adds runtime coverage for workflow-side activity error rehydration/classification.
packages/contract/src/types.ts Adds ErrorDefinition, error payload inference helpers, and portable activity default option types.
packages/contract/src/builder.ts Validates errors maps and strict defaultOptions at defineContract time.
packages/contract/src/index.ts Exports new typed-error and default-options types from the root entry point.
packages/contract/src/errors.ts Adds the @temporal-contract/contract/errors entry point with constructors + rehydration helpers.
packages/contract/src/errors.spec.ts Adds unit tests for error constructor building and failure rehydration behavior.
packages/contract/src/builder.spec.ts Adds tests covering acceptance/rejection of errors maps and defaultOptions.
packages/contract/package.json Exposes ./errors export and includes it in build/dev scripts.
packages/client/src/internal.ts Adds workflow-failure rehydration helper for workflow-level contract errors.
packages/client/src/client.ts Surfaces workflow contract errors in typed client error unions; rehydrates matching failures.
packages/client/src/index.ts Re-exports typed contract error surface (ContractError, unions) from the client entry point.
packages/client/src/client.spec.ts Adds unit tests validating workflow-level typed error rehydration and fallback behavior.
docs/guide/defining-contracts.md Documents typed errors and contract-level default activity options.
docs/guide/activity-handlers.md Documents createContext, typed errors production, and the new middleware API.
docs/guide/worker-implementation.md Documents workflow-side consumption of typed activity errors + option precedence.
docs/guide/client-usage.md Documents typed workflow errors surfacing as ContractError on the typed client.
.changeset/orange-planets-learn.md Adds a minor changeset describing the three new features across packages.
.agents/rules/handlers.md Updates repo guidance to include helpers/middleware and workflow typed-error semantics.
.agents/rules/contract-patterns.md Updates contract pattern guidance with errors and defaultOptions.

Comment on lines +408 to +414
const chain = middlewareChain.reduceRight<typeof invokeImplementation>(
(nextStage, mw) => (stageInput) =>
mw({ ...info, input: stageInput, context: context as TContext }, (...override) =>
nextStage(override.length > 0 ? override[0] : stageInput),
),
invokeImplementation,
);
ActivityOutputValidationError,
} from "./errors.js";
import { contractErrorToApplicationFailure } from "./contract-errors.js";
import { extractHandlerInput } from "./internal.js";
Comment on lines +132 to +136
const contractDefaults = definition.defaultOptions;
const override = overrideByName[name];
if (!contractDefaults && !override) {
continue;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants