Temporal Bun SDK
Learn how to build and operate Temporal workers with the Bun runtime using the @proompteng Temporal Bun SDK.
@proompteng/temporal-bun-sdk is our Bun-first Temporal toolkit. It mirrors the
prix Go worker defaults (namespace default, task queue prix, gRPC port
7233) while layering on:
- Zod-backed environment parsing with TLS, API key, and insecure-mode support.
- Factories for Temporal clients, workflow handles, and worker registration.
- A
temporal-bunCLI that scaffolds projects, checks connectivity, and builds Docker images. - A
temporal-bun-workerbinary that boots a worker with sensible defaults. - Pure TypeScript/Effect runtimes so deployments never depend on the retired Zig
bridge path; older binaries remain archived under
packages/temporal-bun-sdk/brukefor reference only.
Documentation map
This page is the top-level overview. The SDK now documents its feature-set in five logical sections so teams can deep-dive without wading through a single monolith:
- Architecture overview – explains how workflows, workers, clients, and the CLI compose via Effect services and Bun runtime primitives.
- Configuration & operations – covers
loadTemporalConfig, observability knobs, deployment defaults, and advanced TLS/retry tuning. - Tutorials & recipes – workflow/activity examples, proxy helpers, and determinism guardrails.
- CLI & tooling – usage for
temporal-bun init|doctor|docker-build|replayplus proto regeneration and CI guidance.
As new sub-pages come online they will be linked from this section; until then, the corresponding deep dives live below on this page.
Prerequisites
- Bun 1.1.20 or newer (matches the package engine requirement).
- Access to a Temporal Cloud namespace or self-hosted cluster.
- (Optional) The
temporalCLI for namespace administration. dockerif you plan to build container images with the provided helpers.
Install and scaffold
Add the SDK to an existing Bun workspace:
bun add @proompteng/temporal-bun-sdkTo generate a new worker project, run the bundled CLI:
bunx temporal-bun init my-worker
cd my-worker
bun install
bun run dev # runs the worker locallyThe template includes example workflows, activities, and Docker packaging scripts that map one-to-one with the library’s defaults.
Configure your Temporal connection
Configuration flows through loadTemporalConfig(), which reads environment
variables, normalizes paths, and enforces required values. Drop a .env file in
your worker project and tailor the defaults as needed:
TEMPORAL_HOST=temporal.proompteng.local
TEMPORAL_GRPC_PORT=7233
TEMPORAL_NAMESPACE=default
TEMPORAL_TASK_QUEUE=prix
TEMPORAL_API_KEY=your-cloud-api-key
# Uncomment to enable mutual TLS
# TEMPORAL_TLS_CERT_PATH=./certs/client.crt
# TEMPORAL_TLS_KEY_PATH=./certs/client.key
# TEMPORAL_TLS_CA_PATH=./certs/ca.pemEnvironment variables supported by the config loader:
| Variable | Default | Description |
|---|---|---|
TEMPORAL_ADDRESS | ${TEMPORAL_HOST}:${TEMPORAL_GRPC_PORT} | Direct address override (e.g. temporal.example.com:7233). |
TEMPORAL_HOST | 127.0.0.1 | Hostname used when TEMPORAL_ADDRESS is unset. |
TEMPORAL_GRPC_PORT | 7233 | Temporal gRPC port. |
TEMPORAL_NAMESPACE | default | Namespace passed to the worker and client. |
TEMPORAL_TASK_QUEUE | prix | Worker task queue. |
TEMPORAL_API_KEY | unset | Injected into connection metadata for Cloud/API auth. |
TEMPORAL_TLS_CA_PATH | unset | Path to trusted CA bundle. |
TEMPORAL_TLS_CERT_PATH / TEMPORAL_TLS_KEY_PATH | unset | mTLS client certificate & key (require both). |
TEMPORAL_TLS_SERVER_NAME | unset | Overrides TLS server name verification. |
TEMPORAL_CLIENT_RETRY_MAX_ATTEMPTS | 5 | Default WorkflowService RPC attempt budget. |
TEMPORAL_CLIENT_RETRY_INITIAL_MS | 200 | Initial retry delay (milliseconds). |
TEMPORAL_CLIENT_RETRY_MAX_MS | 5000 | Maximum retry delay (milliseconds). |
TEMPORAL_CLIENT_RETRY_BACKOFF | 2 | Exponential backoff multiplier applied per attempt. |
TEMPORAL_CLIENT_RETRY_JITTER_FACTOR | 0.2 | Decorrelated jitter factor between 0 and 1. |
TEMPORAL_CLIENT_RETRY_STATUS_CODES | UNAVAILABLE,DEADLINE_EXCEEDED | Comma-separated Connect codes that should be retried. |
TEMPORAL_ALLOW_INSECURE / ALLOW_INSECURE_TLS | false | Accepts 1/true/on to skip certificate verification. |
TEMPORAL_WORKER_IDENTITY_PREFIX | temporal-bun-worker | Worker identity prefix (host and PID are appended automatically). |
TEMPORAL_LOG_FORMAT | pretty | Select json or pretty logging output for worker/client runs. |
TEMPORAL_LOG_LEVEL | info | Minimum log severity (debug, info, warn, error). |
TEMPORAL_TRACING_INTERCEPTORS_ENABLED | true | Set to false to disable tracing/audit interceptors when environments forbid spans. |
TEMPORAL_METRICS_EXPORTER | in-memory | Choose metrics sink: in-memory, file, prometheus, or otlp. |
TEMPORAL_METRICS_ENDPOINT | unset | Path or URL consumed by file/Prometheus/OTLP exporters. |
TEMPORAL_PAYLOAD_CODECS | unset | Comma-separated payload codecs applied in order (e.g. gzip,aes-gcm). Defaults to JSON-only when omitted. |
TEMPORAL_CODEC_AES_KEY | unset | Base64 or hex AES key (128/192/256-bit) required when aes-gcm is enabled. |
TEMPORAL_CODEC_AES_KEY_ID | default | Optional key identifier recorded in payload metadata for rotation/diagnostics. |
loadTemporalConfig() returns typed values that the client and worker factories
consume directly, so you never have to stitch addresses or TLS buffers together
by hand.
WorkflowService client resilience
createTemporalClient() automatically wraps WorkflowService RPCs with our retry helper and telemetry interceptors:
- Configurable retries –
config.rpcRetryPolicyis populated from theTEMPORAL_CLIENT_RETRY_*env vars (or overrides passed toloadTemporalConfig). All client methods use the resulting jittered exponential backoff policy, and you can override per-call values viaTemporalClientCallOptions.retryPolicy. - Optional call options –
startWorkflow,signalWorkflow,queryWorkflow,signalWithStart,terminateWorkflow, anddescribeNamespaceaccept an optional trailingcallOptionsargument (headers, timeout, abort signal, retry policy). UsetemporalCallOptions()to brand the object so payloads aren’t mistaken for options:import { temporalCallOptions } from '@proompteng/temporal-bun-sdk' await client.signalWorkflow(handle, 'updateState', { payload: { signal: 'start' } }, temporalCallOptions({ headers: { 'x-trace-id': traceId }, timeoutMs: 5_000, })) - Default interceptors – inbound/outbound hooks wrap every workflow RPC and operation: namespace/identity headers are injected, retries use jittered backoff, and latency/error metrics flow through the configured registry/exporter. Tracing spans are opt-in via
TEMPORAL_TRACING_INTERCEPTORS_ENABLED(ortracingEnabledin code). Append custom middleware withclientInterceptors(client) orinterceptors(worker) to add auth headers, audit logs, or bespoke telemetry. - Memo/search helpers –
client.memoandclient.searchAttributesexposeencode/decodehelpers that reuse the client’sDataConverter, making it easy to prepare payloads for raw WorkflowService requests. - TLS validation – TLS buffers are checked up front (missing files, invalid PEMs, and mismatched cert/key pairs throw
TemporalTlsConfigurationError) and transport failures surface asTemporalTlsHandshakeErrorwith remediation hints.
Payload codecs & failure conversion
The SDK’s DataConverter now supports an ordered codec chain so you can compress
and encrypt payloads without giving up deterministic replay:
- Enable codecs with
TEMPORAL_PAYLOAD_CODECS(e.g.gzip,aes-gcm); AES-GCM requiresTEMPORAL_CODEC_AES_KEY(128/192/256-bit, base64/hex) and optionallyTEMPORAL_CODEC_AES_KEY_IDfor rotation tracking. - Codecs wrap the entire payload proto, so replay remains compatible as long as the chain stays stable for a given workflow history.
- Codec metrics are emitted per codec/direction
(
temporal_payload_codec_encode_total_*,*_decode_total_*,*_errors_total_*) and failures log the offending codec/direction. - The failure converter returns a structured
TemporalFailureErrorthat preservesdetailsandcausepayloads using the same codec chain, so workflow/activity/update/query errors decode cleanly on clients. temporal-bun doctornow builds the codec chain from config and fails fast on missing/invalid keys while printing the resolved codec list.
Observability
The Temporal Bun SDK ships with structured logging and metrics layers so you can treat Bun workers/clients like any other service in your stack. Configure the behavior with the same environment variables listed above:
TEMPORAL_LOG_FORMAT– controls the log formatter (prettyorjson).TEMPORAL_LOG_LEVEL– sets the minimum log severity that makes it into the sink.TEMPORAL_METRICS_EXPORTER/TEMPORAL_METRICS_ENDPOINT– select a sink (in-memory,file,prometheus, orotlp) and its path/URL (e.g./tmp/metrics.json).
Want to verify your configuration without running a worker? temporal-bun
includes a doctor command that loads the shared config, builds the interceptor
chain, validates the retry presets, spins up observability services, emits a
log, increments a counter, and flushes the selected exporter:
bunx temporal-bun doctor --log-format=json --metrics=file:/tmp/metrics.jsonThe command prints a success summary (including active interceptors and the resolved retry policy) once the JSON log is emitted and the metrics file is written, so you can script it into CI or pre-deployment checks.
Layered bootstrap helpers
Temporal Bun now exposes first-class Effect Layers so workers, clients, CLI
tools, and tests can share the same managed dependencies without hand-wiring
config or observability plumbing:
createConfigLayer()resolvesTemporalConfigServicevialoadTemporalConfigEffectand accepts overrides/defaults for task queue, namespace, TLS, and worker identity.createObservabilityLayer()wires logger + metrics registries/exporters andcreateWorkflowServiceLayer()constructs the gRPC transport + WorkflowService client with automatic interceptor wiring.createWorkerRuntimeLayer()/runWorkerApp()compose those services with the worker runtime so Bun apps can start/stop workers viaEffect.scopedwithoutAbortControllerplumbing.
Example worker bootstrap with custom overrides:
import { Effect, Layer } from 'effect'
import {
createConfigLayer,
createObservabilityLayer,
createWorkflowServiceLayer,
} from '@proompteng/temporal-bun-sdk/runtime/effect-layers'
import { runWorkerEffect } from '@proompteng/temporal-bun-sdk/worker/layer'
const configLayer = createConfigLayer({
defaults: { address: '10.0.0.5:7233', namespace: 'staging', taskQueue: 'staging-worker' },
})
const observabilityLayer = createObservabilityLayer().pipe(Layer.provide(configLayer))
const workflowLayer = createWorkflowServiceLayer()
.pipe(Layer.provide(configLayer))
.pipe(Layer.provide(observabilityLayer))
await Effect.runPromise(
Effect.scoped(
runWorkerEffect({ workflowsPath: new URL('./workflows/index.ts', import.meta.url).pathname })
.pipe(Layer.provide(configLayer))
.pipe(Layer.provide(observabilityLayer))
.pipe(Layer.provide(workflowLayer)),
),
)Prefer zero-boilerplate? createWorker() and runWorkerApp() wrap the same
layers, discover workflows/activities from the default template, and derive
worker build IDs automatically. For CLI tooling and smoke tests, use
runTemporalCliEffect(effect, options) to execute an Effect program (doctor
checks, replay helpers, diagnostics) against the shared config/observability
stack:
import { Effect } from 'effect'
import { runTemporalCliEffect } from '@proompteng/temporal-bun-sdk/runtime/cli-layer'
import { TemporalConfigService } from '@proompteng/temporal-bun-sdk/runtime/effect-layers'
await runTemporalCliEffect(
Effect.gen(function* () {
const config = yield* TemporalConfigService
console.log('Active namespace:', config.namespace)
}),
{ config: { defaults: { namespace: 'qa' } } },
)Replay workflow histories
temporal-bun replay lets you ingest workflow histories, diff the determinism
state, and share diagnostics with incident responders without writing ad-hoc
scripts. It reuses loadTemporalConfig, the observability sinks, and the same
ingestion pipeline that powers the worker sticky cache.
--history-file <path>– replay a JSON capture (temporal workflow show --history --output jsonor the fixture format described in the replay runbook).--execution <workflowId/runId>– fetch live history via the Temporal CLI or WorkflowService RPC (--source cli|service|auto).--workflow-type,--namespace,--temporal-cli,--json– supply workflow metadata, namespace overrides, a custom CLI binary path, and machine-readable summaries respectively.- Exit codes:
0success,2nondeterminism,1IO/configuration failures.
# Replay a saved history fixture
bunx temporal-bun replay \
--history-file packages/temporal-bun-sdk/tests/replay/fixtures/timer-workflow.json \
--workflow-type timerWorkflow \
--json
# Diff a live execution using the Temporal CLI harness
TEMPORAL_ADDRESS=127.0.0.1:7233 TEMPORAL_NAMESPACE=temporal-bun-integration \
bunx temporal-bun replay \
--execution workflow-id/run-id \
--workflow-type integrationWorkflow \
--namespace temporal-bun-integration \
--source cliSet TEMPORAL_CLI_PATH or pass --temporal-cli when the CLI binary is not on
PATH, and rely on --source service to route through WorkflowService when the
CLI is unavailable (for example, in CI). The command logs history provenance,
event counts, mismatch metadata, and writes a compact JSON summary when
--json is supplied so you can feed the output into other tooling.
Author activities
Activities are plain Bun functions. Keep them deterministic from Temporal’s perspective and delegate external side effects to this layer.
export type Activities = {
echo(input: { message: string }): Promise<string>
sleep(milliseconds: number): Promise<void>
}
export const echo: Activities['echo'] = async ({ message }) => {
return message
}
export const sleep: Activities['sleep'] = async (milliseconds) => {
await Bun.sleep(milliseconds)
}Author workflows
Import workflow primitives from the SDK so Bun can bundle Temporal’s deterministic runtime correctly.
import { proxyActivities } from '@proompteng/temporal-bun-sdk'
import type { Activities } from '../activities.ts'
const activities = proxyActivities<Activities>({
startToCloseTimeout: '1 minute',
})
export async function helloWorkflow(name: string): Promise<string> {
await activities.sleep(10)
return await activities.echo({ message: `Hello, ${name}!` })
}Export your workflows from an index file so the worker can register them all at once:
export * from './workflows.ts'Run a worker
createWorker() wires up the Temporal connection, registers your workflows and
activities, and hands back both the worker instance and the resolved config.
import { fileURLToPath } from 'node:url'
import { createWorker } from '@proompteng/temporal-bun-sdk/worker'
import * as activities from './workers/activities.ts'
const { worker } = await createWorker({
activities,
workflowsPath: fileURLToPath(new URL('./workers/workflows/index.ts', import.meta.url)),
})
const shutdown = async (signal: string) => {
console.log(`Received ${signal}. Shutting down worker…`)
await worker.shutdown()
process.exit(0)
}
process.on('SIGINT', () => void shutdown('SIGINT'))
process.on('SIGTERM', () => void shutdown('SIGTERM'))
await worker.run()For quick tests, run the bundled binary instead of compiling your own entry point:
bunx temporal-bun-workerIt uses the same configuration loader and ships with example workflows if you need a smoke test.
Start and manage workflows from Bun
createTemporalClient() produces a Bun-native Temporal client that already
understands the config loader, workflow handles, and retry policies.
import { createTemporalClient } from '@proompteng/temporal-bun-sdk'
const { client } = await createTemporalClient()
const start = await client.startWorkflow({
workflowId: `hello-${Date.now()}`,
workflowType: 'helloWorkflow',
taskQueue: 'prix',
args: ['Proompteng'],
})
console.log('Workflow started:', start.runId)
await client.signalWorkflow(start.handle, 'complete', { ok: true })
await client.terminateWorkflow(start.handle, { reason: 'demo complete' })
await client.shutdown()All workflow operations (startWorkflow, signalWorkflow, queryWorkflow,
terminateWorkflow, cancelWorkflow, and signalWithStart) share the same
handle structure, so you can persist it between processes without extra
serialization code.
CLI quick reference
The temporal-bun CLI is available through bunx temporal-bun <command> once
the package is installed.
init [directory] [--force]– scaffold a Bun worker project with example workflows, activities, Dockerfile, and scripts.doctor– validate the SDK config, emit a JSON log, and flush the selected metrics exporter.docker-build [--tag <name>]– package the current directory into a worker image using the provided Docker helper.help– print the command reference.
Native bridge status
The pure TypeScript runtime is the default (and only) supported execution path.
Historical Zig bridge assets remain in packages/temporal-bun-sdk/bruke/ for
reference, but environment flags such as TEMPORAL_BUN_SDK_USE_ZIG are no longer
wired into the worker or client. Future experiments should introduce new,
explicit configuration rather than relying on the retired flag.
Local development and production tips
- Use Bun’s
--watchflag (bun run --watch worker.ts) to restart the worker on changes. - Keep activities free of Temporal SDK imports so they remain tree-shakeable and easy to unit-test.
- Expose Prometheus metrics via the worker runtime and forward them to your observability stack.
- Prefer Temporal schedules to cron jobs for recurring workloads.
- Store long-lived credentials in a secrets manager and inject them via the worker environment.
With @proompteng/temporal-bun-sdk, you can reuse existing Temporal workflows
while adopting Bun’s fast startup times and fully typed client/worker helpers.
Architecture overview
- Workflow runtime – executes deterministic workflows entirely inside Bun
with Effect fibers. Command intents (
schedule-activity, timers, child workflows, signals, continue-as-new) emit Temporal protobufs directly and are guarded by a determinism snapshot that captures command order, random values, and logical timestamps. - Worker runtime – wraps pollers, sticky cache routing, activity execution,
and build-id registration. It consumes the same
loadTemporalConfigenvironment contract as our Go worker and exposes concurrency knobs viaTEMPORAL_WORKFLOW_CONCURRENCY,TEMPORAL_ACTIVITY_CONCURRENCY, and sticky-cache variables. - Client – a Connect transport with branded call options, memo/search helpers, TLS diagnostics, and retry policies derived from environment variables. All WorkflowService RPCs run through logging/metrics interceptors so Bun services get observability parity with the worker runtime.
- CLI & tooling –
temporal-bunprovides scaffolding, connectivity checks, Docker packaging, and deterministic replay. The CLI shares the same config and observability layers, ensuring every command fails fast with actionable logs.
Tutorials & recipes
Follow the quickstart above to scaffold workflows/activities, then explore the
example app in packages/temporal-bun-sdk-example for:
- Activity heartbeats and cancellation propagation via
activityContext.heartbeat()andactivityContext.signal.aborted. - Timer, signal, and child workflow orchestration patterns using
proxyActivitiesplusdefineWorkflow. - Deterministic helpers (
determinism.now,determinism.random) that make replay diagnostics trivial.
We are actively porting these recipes into standalone guides (heartbeats, signals, updates, schedules). Each guide links back to runnable snippets so teams can copy/paste into new Bun workers.
CLI & tooling reference
| Command | Purpose | Notes |
|---|---|---|
temporal-bun init | Scaffold a worker + Docker assets | Honors --force to overwrite existing files. |
temporal-bun doctor | Load config, emit log + metrics, verify TLS | Accepts --log-format, --metrics, --metrics-exporter, --metrics-endpoint. |
temporal-bun docker-build | Build worker image | Mirrors docker build -t <tag> -f <file> <context>. |
temporal-bun replay | Diff workflow determinism from JSON or live histories | Supports --history-file, --execution, `--source cli |
Proto regeneration now lives under packages/temporal-bun-sdk/scripts/update-temporal-protos.ts.
Pass --temporal-version <x.y.z> (once implemented) to align with upstream
Temporal drops, and run it before publishing a new SDK version.