Skip to content

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) ​

typescript
function defineContract<T extends ContractDefinition>(definition: T): T;
FieldTypeRequiredDescription
taskQueuestringyesNon-empty, trimmed, ≤ 1000 chars. The polled queue
workflowsRecord<string, WorkflowDefinition>yesAt least one workflow or one global activity
activitiesRecord<string, ActivityDefinition>noGlobal 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):

  • taskQueue empty, missing, with leading/trailing whitespace, or longer than 1000 characters
  • neither a workflow nor a global activity is declared (an empty workflows is 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 defaultOptions or idempotency gets a rename hint
  • a workflow without a valid startPolicy; a workflowId or idempotencyKey that is not a function
  • any key that is not a valid JavaScript identifier (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/), or that is an Object.prototype member (constructor, toString, …)
  • an input, output, or error data that 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 activities block
  • 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 activityOptions timeout / retry-interval — strings are validated against the ms grammar ("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: backoffCoefficient below 1, maximumAttempts not a positive integer (Infinity means unlimited), a zero initialInterval / maximumInterval, or maximumInterval below initialInterval (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) ​

FieldTypeRequired
inputAnySchemayes
outputAnySchemayes
startPolicyWorkflowStartPolicyyes
workflowId(input) => stringno
activitiesRecord<string, ActivityDefinition>no
signalsRecord<string, SignalDefinition>no
queriesRecord<string, QueryDefinition>no
updatesRecord<string, UpdateDefinition>no
searchAttributesRecord<string, SearchAttributeDefinition>no
errorsRecord<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) ​

FieldTypeRequired
inputAnySchemayes
outputAnySchemayes
errorsRecord<string, ErrorDefinition>no
activityOptionsContractActivityOptionsno
idempotencyKey(input) => stringno

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?) ​

FieldTypeRequired
inputAnySchemano

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) ​

FieldTypeRequired
inputAnySchemano
outputAnySchemayes

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) ​

FieldTypeRequired
inputAnySchemano
outputAnySchemayes

Update handlers may be asynchronous. defineUpdate({ output }) declares an argument-less update.

defineSearchAttribute(definition) ​

FieldTypeRequired
kindSearchAttributeKindyes
kindTypeScript 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 ​

typescript
type ErrorDefinition = {
  data?: AnySchema;
  message?: string;
  nonRetryable?: boolean;
};
FieldEffect
the map keyBecomes ApplicationFailure.type on the wire
dataSchema for the payload. Validated when raised and when rehydrated. Omit for a data-less error
messageDefault message. Overridable per instance
nonRetryabletrue stops Temporal retrying. Default false

ContractActivityOptions ​

A strict object — unknown keys are rejected at defineContract time.

typescript
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_000

Merge 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.

typescript
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's cause, never in a modeled E channel
  • ContractError — a declared domain error, carrying errorName and data
  • AnyContractError, ContractErrorUnion, ContractErrorInputUnion, ContractErrorConstructors, ContractErrorOptions
  • ApplicationFailureLike — the structural { type?, message?, details? } shape the rehydration path reads off the wire (a superset of Temporal's ApplicationFailure)

Tag constants and the wire marker ​

  • CONTRACT_ERROR_TAG ("@temporal-contract/ContractError") and TECHNICAL_ERROR_TAG — literal _tag constants, exported from both the package root and /errors, for P.tag(...) matching.
  • CONTRACT_ERROR_WIRE_MARKER — the provenance marker written as details[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 any ApplicationFailure sharing a matching type was 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.

typescript
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.

Next ​

Released under the MIT License.