Upgrade between 8.0 betas
The 8.0 line went through several breaking passes while on the beta tag. This page lists what changed between betas — renames that only ever shipped in a beta, and APIs a beta added and a later beta took back. If you are coming from 7.x, you need none of this: follow Upgrade from 7.x to 8.0, which describes the end state directly.
Sections run oldest first. Apply every section after the beta you are on; the exhaustive matcher and pnpm typecheck point at most of the sites.
unthrown 5 prereleases
If an intermediate beta had you install ts-pattern as a peer (unthrown 5.0.0-beta.5), remove it — unthrown's matcher is built in again as of 5.0.0-beta.6, and unthrown has zero runtime dependencies:
pnpm remove ts-patternThe standalone tag export is gone in unthrown 5.0.0 — it is P.tag now. Drop tag from the import (keeping or adding P) and prefix the call sites:
- import { tag } from "unthrown";
+ import { P } from "unthrown";
result.mapErrCases((matcher) =>
- matcher.with(tag("@temporal-contract/WorkflowFailedError"), (error) => handle(error)),
+ matcher.with(P.tag("@temporal-contract/WorkflowFailedError"), (error) => handle(error)),
);Renames and removals that only shipped in betas
| In an earlier 8.0 beta | Now |
|---|---|
defineWorkflow({ idempotency }) | defineWorkflow({ startPolicy }) — same three values |
IdempotencyMode (type, then a deprecated alias) | WorkflowStartPolicy, exported from the package root |
propagateActivityFailure | propagateFailure (it always covered child workflows and cancellation scopes too) |
qualifyFailure("X") | qualifyFailure("X", { expected }) — expected: "any" keeps the old catch-all deliberately |
WORKFLOW_START_ERROR_TAGS / WORKFLOW_OUTCOME_ERROR_TAGS / WORKFLOW_RESULT_ERROR_TAGS, tagPatterns(tags) | the pattern groups WORKFLOW_START_PATTERNS, WORKFLOW_EXECUTE_PATTERNS, WORKFLOW_RESULT_PATTERNS, … — matcher.with(...WORKFLOW_RESULT_PATTERNS, handler) |
createContractTest(contract, { workflowsPath, activities }) | createContractTest({ contract, workflowsPath, activities }) |
runActivity(definition, implementation, input) | runActivity(definition, { implementation, input }) |
activity leaf ({ errors, context }, args) reading args | unchanged, and the first record now also carries input: ({ errors, input }) => ... |
createContractTest's worker fixture is the TypedWorker (the raw Temporal Worker is at worker.raw).
Compile-time contract validation is gone
A beta mirrored defineContract's runtime checks at the type level (reserved names, ms durations, activity-name collisions) and made its type parameter const. Both are removed; the runtime checks were always authoritative and still run at defineContract call time. A contract assembled inside a generic helper type-checks again, so drop any defineContract(contract as never) workaround.
Audit hardening (the release after 8.0.0-beta.11)
A pre-release audit tightened every package. Each item says what to change.
Contract
- Validation messages list paths only.
summarizeIssues— and so themessageof every client and worker validation error, which lands in Temporal history unencrypted — names the failing paths, capped at five (at email; at items[0].qty; …and 3 more,at rootfor the value itself). Schema messages embed raw input values. Readerror.issuesfor the detail, and update any assertion on the oldat path: messagetext. ContractDefinitionError. Definition failures throw this exportedErrorsubclass with a dottedpath(workflows.processOrder.signals.cancel); messages readContract validation failed at <path>: …. Match oninstanceof ContractDefinitionError/path, not the message text.- Strict keys on every definition — workflow, activity, signal, query, update, error, and search-attribute definitions reject unknown keys (also at compile time on the
define*builders). A leftoverdefaultOptionsoridempotencygets a rename hint;workflowIdandidempotencyKeymust be functions. startPolicyis required at runtime, not just by type. A beta deliberately accepted a missingstartPolicyat runtime; add it to any workflow assembled outside the type system.- Retry policies are validated like Temporal does:
backoffCoefficientbelow 1, amaximumAttemptsthat is not a positive integer (Infinitystays allowed), a zero interval, ormaximumIntervalbelowinitialInterval(1s when unset) fail atdefineContractinstead of failing the workflow task. - More reserved names —
Object.prototypemembers (constructor,toString, …) as any name; the worker's own failure types (WorkflowInputValidationError,ContractMisuseError, …) as error names; Temporal system attributes (WorkflowId,ExecutionStatus, …) as search-attribute names; and one search attribute declared with two kinds across workflows. Rename. taskQueuemay not have leading/trailing whitespace or exceed 1000 characters.onRehydrationMissis removed from@temporal-contract/contract/errors— its module-level handler never fired inside the bundled workflow sandbox. Delete the registration: the worker logs each miss through the workflow logger (log.warn), and the client takes a hook inTypedClient.create({ client, onRehydrationMiss }). TheRehydrationMisstype stays exported.IdempotencyModeis removed — useWorkflowStartPolicy.
Client
getHandlereturns the handle directly, like Temporal's:orders.getHandle(name, id).getOrThrow()→orders.getHandle(name, id).WorkflowNotInContractErroris removed — its class, itsWORKFLOW_NOT_IN_CONTRACT_ERROR_TAG, and its slot in every union and inWORKFLOW_START_PATTERNS/WORKFLOW_EXECUTE_PATTERNS/SCHEDULE_CREATE_PATTERNS. The types only admit declared names, so an undeclared workflow, signal, or update name is a defect carrying aTechnicalError. Delete theP.tag(...)arms.- The contract owns the start options.
workflowIdReusePolicyandfollowRunsare no longer accepted bystartWorkflow/signalWithStart/executeWorkflow(norfollowRunsbygetHandle): the contract'sstartPolicyalways wins, and handles always follow the run chain.signalWithStartenforcesworkflowIdlikestartWorkflow— forbidden when the contract derives it, required otherwise. - Start handles'
runIdisundefined— they follow the run chain. ReadfirstExecutionRunIdfor the started run. executeWorkflowisstartWorkflow(...)thenhandle.result(), so its result-phase errors name the derived workflow ID, and an unrecognized result failure'sRuntimeClientError.operationis"result".- Update errors match the SDK.
startUpdateerrs only withUpdateValidationError | UpdateRpcTimeoutOrCancelledError | WorkflowExecutionNotFoundError; a rejection or a failed handler surfaces on the update handle'sresult(). The newUpdateRpcTimeoutOrCancelledErrorjoins every update union andUPDATE_PATTERNS— add an arm. - Temporal's
QueryRejectedError(a query refused because of the execution's status) surfaces asQueryFailedError. - Search-attribute values are checked against their declared kind at runtime again (a defect, like an undeclared key).
schedule.updatere-checks the action's search attributes and refuses to move a contract workflow off the contract's task queue. - Schedules:
schedule.create'sargsis optional when the input schema acceptsundefined;TypedScheduleHandle.rawexposes the SDK handle. - New:
handle.updates.name(input, { updateId }),handle.getUpdateHandle(updateName, updateId),contractClient.executeUpdateWithStart(...),contractClient.workflowIdFor(workflowName, input),WorkflowFailedError.retryState, and the re-exportedCONTRACT_ERROR_TAG/RehydrationMiss. - Peers: install
@temporal-contract/contract@betaalongside the client;@temporalio/client/@temporalio/common^1.24.0,unthrown^5.11.0.
Worker
- Library errors thrown from workflow code fail the workflow.
throw result.error/.getOrThrow()on anActivityError,ChildWorkflowError,*CancelledError, orChildWorkflowNotFoundErrorused to retry the workflow task forever;declareWorkflowand signal/update handlers now map them to the Temporal failure they carry, the same mapping aspropagateFailure. A thrown update-handler error rejects the update. context.infois live — a getter overworkflowInfo(), socontinueAsNewSuggested/historyLengthupdate between activations.- Child workflows follow the contract. A child whose definition declares
workflowIdderives it fromargs, andoptions.workflowIdis a type error — drop it. The child's declared errors rehydrate into typedContractErrors onexecuteChildWorkflow/handle.result()(newChildWorkflowContractErrorsOf), andcontext.sagacompensates on them — handle the new members on the error channel. - No per-call
workflowIdReusePolicyon child workflows — change the child'sstartPolicyinstead. ContractMisuseErrorfor structural misuse: an undeclaredContractErrorname (wasContractErrorDataValidationErrorwith a fake issue), acontinueAsNewtarget not on the contract (wasWorkflowInputValidationError), anddeclareActivitiesHandler's missing, conflicting, or ambiguous implementations (were plainError). Also exported from./activity. Match onContractMisuseError/type === "ContractMisuseError".TypedWorker.createfails on a misdeclared workflows module — aContractMisuseErrorthrown bydeclareWorkflowat import is aTechnicalErrordefect instead of being skipped.TypedWorker.shutdown()returnsResult<void, never>— calling it on a worker that is not running is a defect, not a throw. Useworker.shutdown().get()to keep throwing.- Validation failures carry their issues in
details[0]({ message, path }, encrypted by payload codecs) now thatmessagelists paths only. - No bind-time sync-schema probe. An async query/update input schema is rejected on the first query/update, not when the handler is bound.
qualifyFailurenever wraps a cancellation (CancelledFailure,AbortError), even withexpected: "any"— it rides the defect channel and stays a cancellation.rethrowCancellation/propagateFailurethrow a freshCancelledFailurefor a cancellation error with nocause.- Scope typing.
cancellableScope/nonCancellableScoperesolve toAwaited<T>: a returnedAsyncResultis adopted, so the scope's value is its settledResult. - Peers: install
@temporal-contract/contract@betaalongside the worker;@temporalio/*^1.24.0,unthrown^5.11.0.
Testing
createContractTestruns each call (each test file, in practice) in its own Temporal namespace, registered on the testcontainers server and used by the worker and the clients, so parallel files no longer steal each other's tasks. The newnamespacefixture names it; aworkerOptions.namespacereplaces it and must already exist. The workflows are bundled once per file, andworkerOptionsno longer acceptsworkflowBundle— drop it.testRig/createTimeSkippingContractTestreplay workflows whose contract derives the workflow ID (resolved withworkflowIdFor) and recordexecuteUpdateWithStartstarts.- Teardown waits for the worker to stop from any state (
STOPPING/DRAINING/DRAINEDincluded), with a timeout, and checksTypedWorker.shutdown()'s result. runActivityHandlerround-trips the input, the output, and the failure details through the payload converter (defaultPayloadConverter, or the newpayloadConverteroption). A value that does not survive serialization — aDateunder the JSON converter — now fails withActivityOutputValidationError, as in production. An undeclared error name surfacesContractMisuseError.RunActivityImplementation's helpers are the worker'sActivityImplementationHelpers, soidempotencyKeycan be destructured.- Global setup: a container that fails to start stops the ones already started; health checks retry 60 times by default (
healthCheckRetries); the default images are pinned by digest; PostgreSQL gets a random password per run and no host port; the Temporal health check uses thetemporalCLI instead oftctl. - Global setup's startup budget is
startupTimeoutMs(the release after8.0.0-beta.12), default three minutes per container, and it reaches testcontainers' own startup timeout, which used to cap the wait at 60 seconds whateverhealthCheckRetriessaid.healthCheckRetriesis deprecated and read as that budget in seconds. - Internal helpers left the public entries:
resolveTemporalAddress(/extension) andisTerminalStatus,START_METHODS,skipReasonFor,extractStartedWorkflowId(/test-rig). Copy them if you relied on them. - Peers:
@temporalio/*^1.24.0,unthrown^5.11.0;vitestand@temporalio/testingare now optional —/activityand/workflow-bundleneed novitest, and only/activity,/time-skipping, and/test-rigneed@temporalio/testing.