Worker & deployment
createWorker() runs your workflows and activities: one Temporal worker per task queue, a workflow bundle built at startup or ahead of time, and graceful shutdown on SIGTERM.
import { createWorker } from '@norn-ts/norn/worker';import { PaymentActivities } from './activities/payment.activities';import { StripeGateway } from './services/stripe';
const worker = await createWorker({ workflows: new URL('./workflows/index.ts', import.meta.url), activities: { payments: new PaymentActivities(new StripeGateway(process.env.STRIPE_KEY!)) },});await worker.run(); // resolves after a graceful shutdown; rejects if a worker failsworkflowsis the module that exports your@Workflowclasses. The worker imports it once to learn their task queues, then bundles it for the workflow sandbox.activitiesmaps a namespace to an object. Every method ofactivities.paymentsis registered aspayments.<method>, the namesuseActivities<PaymentActivities>('payments')schedules. Build your dependencies here, with plain constructors.
Connection settings
Section titled “Connection settings”| Variable | Option | Default | Description |
|---|---|---|---|
TEMPORAL_ADDRESS |
address |
localhost:7233 |
Temporal frontend address (host:port). |
TEMPORAL_NAMESPACE |
namespace |
default |
Namespace to poll. |
TEMPORAL_API_KEY |
apiKey |
none | API key (Temporal Cloud). Turns TLS on. |
TEMPORAL_TLS |
tls |
false |
true turns TLS on without an API key. |
NORN_WORKFLOW_BUNDLE |
workflowBundle |
bundle at startup | Path of a prebuilt workflow bundle. Ignored by an activity-only worker. |
Options win over environment variables, and empty variables count as unset. For mTLS client certificates, connect yourself and pass the connection:
import { readFile } from 'node:fs/promises';import { NativeConnection } from '@temporalio/worker';
const connection = await NativeConnection.connect({ address: process.env.TEMPORAL_ADDRESS!, tls: { clientCertPair: { crt: await readFile('/certs/client.pem'), key: await readFile('/certs/client.key') } },});const worker = await createWorker({ workflows, activities, connection });// Norn doesn't close connections it didn't open.createWorker() options
Section titled “createWorker() options”| Option | Description |
|---|---|
workflows |
The module exporting your @Workflow classes, as a URL or path. |
activities |
Activity objects by namespace. |
taskQueues |
Queues to poll. Defaults to every queue the workflows declare; required for an activity-only worker. |
address, namespace, apiKey, tls |
Connection settings, as above. |
connection |
An existing NativeConnection. |
workflowBundle |
{ code } or { codePath } from bundleWorkflows(). |
dataConverter |
A custom data converter, such as one that encrypts payloads. |
plugins |
Temporal worker plugins, such as OpenTelemetry. Also applied when bundling. |
shutdownGraceTime |
How long in-flight activities may run after shutdown starts. Default 30s. |
handleSignals |
Shut down gracefully on SIGTERM and SIGINT. Default true. |
workerOptions |
Passed through to every Worker.create(): concurrency limits, pollers, interceptors, workerDeploymentOptions for worker versioning, and so on. |
Lifecycle and graceful shutdown
Section titled “Lifecycle and graceful shutdown”createWorker() resolves once the connection is open, the bundle is loaded and every Temporal worker is created. Its state is then INITIALIZED. run() starts polling (RUNNING) and resolves when the worker has stopped. On SIGTERM or SIGINT, or when you call shutdown():
- The state becomes
STOPPING, and every Temporal worker stops polling. - In-flight workflow tasks finish. In-flight activities get up to
shutdownGraceTimeto complete, after which they’re cancelled (activities that heartbeat see the cancellation). - Norn closes the connection it opened.
run()resolves, the state isSTOPPED, and your process exits normally.
If one task queue’s worker fails, Norn stops the others and run() rejects (state FAILED), so the process exits and your orchestrator restarts it. await using worker = await createWorker(...) shuts down when the scope ends.
Give your platform at least the grace time plus a margin to stop the container (stopTimeout on ECS, terminationGracePeriodSeconds on Kubernetes). If it kills the process first, in-flight activities are retried elsewhere after their timeouts, which is safe because activities must be idempotent, but slow.
Scaling task queues
Section titled “Scaling task queues”By default one process polls every task queue your workflows declare. To scale queues independently, run the same code with different taskQueues, for example from an environment variable:
const worker = await createWorker({ workflows, activities, taskQueues: process.env.TASK_QUEUES?.split(','),});Production
Section titled “Production”In development, tsx runs your TypeScript and the worker bundles the workflows at startup. In production, compile first. The starter’s pnpm build compiles the worker and the workflows module with esbuild, and pnpm start runs it:
esbuild src/worker.ts src/workflows/index.ts --bundle --platform=node --target=node22 --format=esm --packages=external --outdir=distnode dist/worker.js--target=node22 matters: it lowers the decorators, which Node can’t run yet. The compiled worker still names ./workflows/index.ts; when that file doesn’t exist, Norn loads the compiled dist/workflows/index.js next to it.
Prebuilt workflow bundle
Section titled “Prebuilt workflow bundle”Bundling at startup takes a few seconds and needs webpack at runtime. To skip it, build the bundle once at build time and point NORN_WORKFLOW_BUNDLE at it:
// scripts/bundle.ts, run with tsximport { bundleWorkflows } from '@norn-ts/norn/worker';
await bundleWorkflows(new URL('../src/workflows/index.ts', import.meta.url), { outFile: 'dist/workflow-bundle.js' });NORN_WORKFLOW_BUNDLE=dist/workflow-bundle.js node dist/worker.jsWith a prebuilt bundle the worker writes nothing to disk, so it runs on a read-only filesystem. Bundling at startup writes a small entry file to the OS temp directory.
Dockerfile
Section titled “Dockerfile”# syntax=docker/dockerfile:1FROM node:24-slim AS buildWORKDIR /appRUN corepack enableCOPY package.json pnpm-lock.yaml ./RUN pnpm install --frozen-lockfileCOPY . .RUN pnpm run build && pnpm exec tsx scripts/bundle.ts && pnpm prune --prod
FROM node:24-slimWORKDIR /appENV NODE_ENV=production NORN_WORKFLOW_BUNDLE=/app/dist/workflow-bundle.jsCOPY --from=build /app/package.json ./COPY --from=build /app/node_modules ./node_modulesCOPY --from=build /app/dist ./distUSER node# Exec form, so node is PID 1 and receives SIGTERM directly.CMD ["node", "dist/worker.js"]Use a Debian-based image such as node:24-slim, because the Temporal SDK includes a native module built for glibc. TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE and TEMPORAL_API_KEY come from your deployment.
Metrics, logs and health
Section titled “Metrics, logs and health”Norn leaves these to the Temporal SDK. Install the SDK runtime before creating the worker:
import { Runtime } from '@temporalio/worker';
Runtime.install({ telemetryOptions: { metrics: { prometheus: { bindAddress: '0.0.0.0:9464' } } },});For a liveness check, watch the process. A failed worker makes run() reject and the process exit, and worker.state is there if you serve your own health endpoint.