Contract surface
Everything exported from @temporal-contract/contract.
For generated per-symbol documentation, see the API reference.
Builders
Resource builders preserve their definitions and literal types. defineContract validates a composed contract; defineContractBinding validates a new composition with a deployment task queue.
defineContract(definition)
function defineContract<T extends ContractDefinition>(definition: T): T;| Field | Type | Required | Description |
|---|---|---|---|
taskQueue | string | yes | Non-empty, trimmed, ≤ 1000 chars. The polled queue |
workflows | Record<string, WorkflowDefinition> | yes | At least one workflow or one global activity |
activities | Record<string, ActivityDefinition> | no | Global activities, reachable from every workflow |
Throws ContractDefinitionError (exported from the package root, a plain Error subclass with a dotted path to the offending slot, "" at the root) when the structure is invalid. The check is a hand-rolled structural validator — the contract package has no runtime schema-library dependency. Checked at call time (first failure wins):
taskQueueempty, missing, with leading/trailing whitespace, or longer than 1000 characters- neither a workflow nor a global activity is declared (an empty
workflowsis fine when at least one global activity exists — see activity-only contracts below) - an unknown key at the contract root (strict — only the three fields above) or on any workflow / activity / signal / query / update / error / search-attribute definition; a leftover
defaultOptionsoridempotencygets a rename hint - a workflow without a valid
startPolicy; aworkflowIdoridempotencyKeythat is not a function - any key that is not a valid JavaScript identifier (
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/), or that is anObject.prototypemember (constructor,toString, …) - an
input,output, or errordatathat is not Standard Schema compatible - an activity name that collides across the flat namespace — global vs workflow-scoped, or two different definitions under one name across workflows. Reusing the same definition object across workflows is allowed (one activity, not a collision); the collision message recommends hoisting shared activities to the global
activitiesblock - a workflow name colliding with a global activity name (they share the root of the worker's implementations map)
- unknown keys inside
activityOptions - a reserved name — any workflow / activity / signal / query / update name starting with
__temporal_, or the exact names__stack_trace/__enhanced_stack_trace(used internally by the Temporal SDK); an error name the worker uses for its own failures (WorkflowInputValidationError,ContractMisuseError, …); a search attribute named like a Temporal system attribute (WorkflowId,ExecutionStatus, …) - a search attribute declared with different
kinds in two workflows - an invalid duration in an
activityOptionstimeout / retry-interval — strings are validated against themsgrammar ("30s","5 minutes","1.5h", or a long-form unit), so"5 minutos","", and"abc"throw at definition; a numeric duration must be a non-negative finite number of milliseconds - an invalid retry policy — rejected exactly where Temporal would reject it when the activity is scheduled:
backoffCoefficientbelow 1,maximumAttemptsnot a positive integer (Infinitymeans unlimited), a zeroinitialInterval/maximumInterval, ormaximumIntervalbelowinitialInterval(Temporal's 1s default when unset)
Activity-only contracts are allowed. workflows may be {} as long as at least one global activity is declared — the "at least one workflow" rule above relaxes when the contract exists purely to serve activities.
defineWorkflow(definition)
| Field | Type | Required |
|---|---|---|
input | AnySchema | yes |
output | AnySchema | yes |
startPolicy | WorkflowStartPolicy | yes |
workflowId | (input) => string | no |
activities | Record<string, ActivityDefinition> | no |
signals | Record<string, SignalDefinition> | no |
queries | Record<string, QueryDefinition> | no |
updates | Record<string, UpdateDefinition> | no |
searchAttributes | Record<string, SearchAttributeDefinition> | no |
errors | Record<string, ErrorDefinition> | no |
startPolicy governs what happens when this workflow ID is started again after a previous run has closed — "once-per-id" (REJECT_DUPLICATE), "retry-if-failed" (ALLOW_DUPLICATE_FAILED_ONLY — re-runnable after any Closed state other than Completed: Failed, Cancelled, Terminated, or TimedOut), or "allow-duplicate" (ALLOW_DUPLICATE, Temporal's own default). The client applies it to every startWorkflow / executeWorkflow / signalWithStart / executeUpdateWithStart, and the worker applies it to every context.startChildWorkflow / context.executeChildWorkflow of that workflow; neither accepts a per-call workflowIdReusePolicy. It is not applied to schedule.create — the schedule action type has no workflowIdReusePolicy field, so a schedule action pinning a fixed workflowId gets Temporal's own default (ALLOW_DUPLICATE) regardless of this mode; see Schedule workflows for the implications. workflowIdConflictPolicy — what to do about a run that is already open — stays a per-call client/worker option, untouched by this field. See Define a contract.
workflowId moves the workflow ID from the caller to the contract: it receives the validated input and must be pure. Every startWorkflow / executeWorkflow / signalWithStart / executeUpdateWithStart, and every child-workflow start from the worker, computes the ID from the payload, and passing one explicitly is a type error. contractClient.workflowIdFor(name, input) computes it without starting anything, to getHandle an existing execution. Not applied to schedule.create, which generates one ID per firing.
defineActivity(definition)
| Field | Type | Required |
|---|---|---|
input | AnySchema | yes |
output | AnySchema | yes |
errors | Record<string, ErrorDefinition> | no |
activityOptions | ContractActivityOptions | no |
idempotencyKey | (input) => string | no |
idempotencyKey derives a key from the validated input; the worker hands it to the implementation as helpers.idempotencyKey (string when declared, undefined otherwise). Stable across activity retries, worker crashes, and a fresh execution with the same input — key it on the operation's identity (`charge:${orderId}`), not on its parameters.
Renamed in 8.0
The contract-level activity-options field is activityOptions (it was defaultOptions before 8.0), and its type is ContractActivityOptions (it was ActivityDefaultOptions). The type name is now distinct from Temporal's own ActivityOptions, which is what the worker-side overrides take.
defineSignal(definition?)
| Field | Type | Required |
|---|---|---|
input | AnySchema | no |
Signals return nothing, so there is no output.
Omit input (or the whole argument: defineSignal()) for a payload-less signal — the definition then carries an UndefinedInputSchema, the handler receives undefined, and the client-side payload argument is omittable.
defineQuery(definition)
| Field | Type | Required |
|---|---|---|
input | AnySchema | no |
output | AnySchema | yes |
Synchronous validation only
Temporal requires query handlers to complete synchronously, so both schemas must validate synchronously. Async refinements are not supported. Standard Schema does not expose the distinction at the type level, so the worker checks every call at runtime and fails that query with a ContractMisuseError if ~standard.validate returns a Promise.
Use defineQuery({ output }) for a query with no parameters.
defineUpdate(definition)
| Field | Type | Required |
|---|---|---|
input | AnySchema | no |
output | AnySchema | yes |
Update handlers may be asynchronous. defineUpdate({ output }) declares an argument-less update.
defineSearchAttribute(definition)
| Field | Type | Required |
|---|---|---|
kind | SearchAttributeKind | yes |
kind | TypeScript type |
|---|---|
"TEXT" | string |
"KEYWORD" | string |
"INT" | number |
"DOUBLE" | number |
"BOOL" | boolean |
"DATETIME" | Date |
"KEYWORD_LIST" | string[] |
TEXT is tokenized and analyzed; KEYWORD matches exactly. Attributes must be registered on the Temporal namespace before use.
Option types
ErrorDefinition
type ErrorDefinition = {
data?: AnySchema;
message?: string;
nonRetryable?: boolean;
};| Field | Effect |
|---|---|
| the map key | Becomes ApplicationFailure.type on the wire |
data | Schema for the payload. Validated when raised and when rehydrated. Omit for a data-less error |
message | Default message. Overridable per instance |
nonRetryable | true stops Temporal retrying. Default false |
ContractActivityOptions
A strict object — unknown keys are rejected at defineContract time.
type ContractActivityOptions = {
startToCloseTimeout?: DurationValue;
scheduleToCloseTimeout?: DurationValue;
scheduleToStartTimeout?: DurationValue;
heartbeatTimeout?: DurationValue;
retry?: ActivityRetryPolicy;
};
type ActivityRetryPolicy = {
initialInterval?: DurationValue;
maximumInterval?: DurationValue;
backoffCoefficient?: number;
maximumAttempts?: number;
nonRetryableErrorTypes?: string[];
};
type DurationValue = string | number; // "30 seconds" | 30_000Merge order, least to most specific: declareWorkflow({ activityOptions }) → defineActivity({ activityOptions }) → declareWorkflow({ activityOptionsByName }). See Tune activity options.
Formatting helpers
formatIssue(issue)
Renders one Standard Schema issue as a single readable line — path and the schema's message verbatim. Schema libraries embed raw input values in their messages, so use it for in-process diagnostics only.
summarizeIssues(issues)
Renders an array of issues as a redacted summary — the failing paths only, capped at five (at items[0].quantity; at email; …and 2 more, at root for the value itself). This is what the client and worker validation errors put in message, which lands in Temporal history unencrypted; the full issues stay on the errors' issues property.
import { summarizeIssues } from "@temporal-contract/contract";
if (result.isErr() && result.error instanceof WorkflowValidationError) {
console.error(summarizeIssues(result.error.issues));
}Errors
Exported from @temporal-contract/contract/errors, and re-exported by the worker and client packages so you rarely import from here directly. (The package root does not import unthrown, so the runtime error machinery lives on this separate /errors entry; the root re-exports only the two tag constants below.)
TechnicalError— infrastructure fault. Only ever a defect'scause, never in a modeledEchannelContractError— a declared domain error, carryingerrorNameanddataAnyContractError,ContractErrorUnion,ContractErrorInputUnion,ContractErrorConstructors,ContractErrorOptionsApplicationFailureLike— the structural{ type?, message?, details? }shape the rehydration path reads off the wire (a superset of Temporal'sApplicationFailure)
Tag constants and the wire marker
CONTRACT_ERROR_TAG("@temporal-contract/ContractError") andTECHNICAL_ERROR_TAG— literal_tagconstants, exported from both the package root and/errors, forP.tag(...)matching.CONTRACT_ERROR_WIRE_MARKER— the provenance marker written asdetails[1] = { $tc: 1 }when a contract error crosses the wire (details[0]is the validated data). A data-less declared error now requires this marker to rehydrate — closing the false positive where anyApplicationFailuresharing a matchingtypewas surfaced as the typed error.
Rehydration misses
When an inbound ApplicationFailure matches a declared error's type but fails to rehydrate — data that does not validate, or a data-less error missing the wire marker — it degrades to a generic failure. The worker logs each activity / child-workflow miss through the workflow logger (log.warn); the client reports its own to the onRehydrationMiss callback passed to TypedClient.create, which receives a RehydrationMiss (exported). Watch both to catch contract/producer drift.
See the errors reference.
Types
Contract shapes
AnySchema, UndefinedInputSchema, ActivityDefinition, SignalDefinition, QueryDefinition, UpdateDefinition, WorkflowDefinition, AnyWorkflowDefinition, ContractDefinition, SearchAttributeDefinition, SearchAttributeKind, SearchAttributeKindToType, WorkflowStartPolicy
UndefinedInputSchema is the Standard Schema type materialized by defineSignal / defineQuery / defineUpdate when input is omitted — validation only accepts undefined.
Error inference
ErrorDefinition, InferDeclaredErrors, InferErrorData, InferErrorDataInput
InferErrorData gives the schema's output (post-transform) shape — what a consumer receives. InferErrorDataInput gives the input (pre-transform) shape — what a producer passes to the constructor.
Renamed in 8.0
DeclaredErrorsOf is now InferDeclaredErrors (part of the Infer* naming sweep).
Name extraction
InferWorkflowNames, InferActivityNames, InferSignalNames, InferQueryNames, InferUpdateNames
Renamed in 8.0
SignalNamesOf / QueryNamesOf / UpdateNamesOf are now InferSignalNames / InferQueryNames / InferUpdateNames.
Direction-aware inference
WorkerInferInput, WorkerInferOutput, ClientInferInput, ClientInferOutput
Schemas can transform, so the type differs by direction. A client passes the schema's input type and receives its output type; a worker receives the output type and returns something assignable to the input type. These primitives encode that and are shared by the worker and client packages.
Runtime deployment binding
defineContractBinding(contract, { taskQueue }) returns a new validated contract. The original queue stays unchanged, while workflow/activity definitions, schema instances, declared errors and start policies retain their identity and inference. Use the result with typed clients, workers, child calls and schedules.
import { defineContractBinding } from "@temporal-contract/contract";
// ordersContract is the application's shared protocol contract.
const release = defineContractBinding(ordersContract, { taskQueue: "orders.release-a" });The workflow type name remains stable across queues. Execution IDs are unique within a namespace, so keep the business operation ID rather than adding a release suffix. Explicit activity queue overrides remain explicit; the binding does not recursively rewrite them. Bind before starting work, or from recorded workflow input/activity results. Never resolve a mutable deployment environment inside replaying workflow code. Keep old workers available for their histories, or perform an explicitly compatible worker-version migration.