proompteng

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-bun CLI that scaffolds projects, checks connectivity, and builds Docker images.
  • A temporal-bun-worker binary 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/bruke for 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|replay plus 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 temporal CLI for namespace administration.
  • docker if 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-sdk

To 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 locally

The 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.pem

Environment variables supported by the config loader:

VariableDefaultDescription
TEMPORAL_ADDRESS${TEMPORAL_HOST}:${TEMPORAL_GRPC_PORT}Direct address override (e.g. temporal.example.com:7233).
TEMPORAL_HOST127.0.0.1Hostname used when TEMPORAL_ADDRESS is unset.
TEMPORAL_GRPC_PORT7233Temporal gRPC port.
TEMPORAL_NAMESPACEdefaultNamespace passed to the worker and client.
TEMPORAL_TASK_QUEUEprixWorker task queue.
TEMPORAL_API_KEYunsetInjected into connection metadata for Cloud/API auth.
TEMPORAL_TLS_CA_PATHunsetPath to trusted CA bundle.
TEMPORAL_TLS_CERT_PATH / TEMPORAL_TLS_KEY_PATHunsetmTLS client certificate & key (require both).
TEMPORAL_TLS_SERVER_NAMEunsetOverrides TLS server name verification.
TEMPORAL_CLIENT_RETRY_MAX_ATTEMPTS5Default WorkflowService RPC attempt budget.
TEMPORAL_CLIENT_RETRY_INITIAL_MS200Initial retry delay (milliseconds).
TEMPORAL_CLIENT_RETRY_MAX_MS5000Maximum retry delay (milliseconds).
TEMPORAL_CLIENT_RETRY_BACKOFF2Exponential backoff multiplier applied per attempt.
TEMPORAL_CLIENT_RETRY_JITTER_FACTOR0.2Decorrelated jitter factor between 0 and 1.
TEMPORAL_CLIENT_RETRY_STATUS_CODESUNAVAILABLE,DEADLINE_EXCEEDEDComma-separated Connect codes that should be retried.
TEMPORAL_ALLOW_INSECURE / ALLOW_INSECURE_TLSfalseAccepts 1/true/on to skip certificate verification.
TEMPORAL_WORKER_IDENTITY_PREFIXtemporal-bun-workerWorker identity prefix (host and PID are appended automatically).
TEMPORAL_LOG_FORMATprettySelect json or pretty logging output for worker/client runs.
TEMPORAL_LOG_LEVELinfoMinimum log severity (debug, info, warn, error).
TEMPORAL_TRACING_INTERCEPTORS_ENABLEDtrueSet to false to disable tracing/audit interceptors when environments forbid spans.
TEMPORAL_METRICS_EXPORTERin-memoryChoose metrics sink: in-memory, file, prometheus, or otlp.
TEMPORAL_METRICS_ENDPOINTunsetPath or URL consumed by file/Prometheus/OTLP exporters.
TEMPORAL_PAYLOAD_CODECSunsetComma-separated payload codecs applied in order (e.g. gzip,aes-gcm). Defaults to JSON-only when omitted.
TEMPORAL_CODEC_AES_KEYunsetBase64 or hex AES key (128/192/256-bit) required when aes-gcm is enabled.
TEMPORAL_CODEC_AES_KEY_IDdefaultOptional 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 retriesconfig.rpcRetryPolicy is populated from the TEMPORAL_CLIENT_RETRY_* env vars (or overrides passed to loadTemporalConfig). All client methods use the resulting jittered exponential backoff policy, and you can override per-call values via TemporalClientCallOptions.retryPolicy.
  • Optional call optionsstartWorkflow, signalWorkflow, queryWorkflow, signalWithStart, terminateWorkflow, and describeNamespace accept an optional trailing callOptions argument (headers, timeout, abort signal, retry policy). Use temporalCallOptions() 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 (or tracingEnabled in code). Append custom middleware with clientInterceptors (client) or interceptors (worker) to add auth headers, audit logs, or bespoke telemetry.
  • Memo/search helpersclient.memo and client.searchAttributes expose encode/decode helpers that reuse the client’s DataConverter, 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 as TemporalTlsHandshakeError with 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 requires TEMPORAL_CODEC_AES_KEY (128/192/256-bit, base64/hex) and optionally TEMPORAL_CODEC_AES_KEY_ID for 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 TemporalFailureError that preserves details and cause payloads using the same codec chain, so workflow/activity/update/query errors decode cleanly on clients.
  • temporal-bun doctor now 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 (pretty or json).
  • 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, or otlp) 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.json

The 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() resolves TemporalConfigService via loadTemporalConfigEffect and accepts overrides/defaults for task queue, namespace, TLS, and worker identity.
  • createObservabilityLayer() wires logger + metrics registries/exporters and createWorkflowServiceLayer() 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 via Effect.scoped without AbortController plumbing.

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 json or 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: 0 success, 2 nondeterminism, 1 IO/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 cli

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

workers/activities.ts
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.

workers/workflows.ts
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:

workers/workflows/index.ts
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.

worker.ts
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-worker

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

scripts/start-workflow.ts
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 --watch flag (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 loadTemporalConfig environment contract as our Go worker and exposes concurrency knobs via TEMPORAL_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 & toolingtemporal-bun provides 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() and activityContext.signal.aborted.
  • Timer, signal, and child workflow orchestration patterns using proxyActivities plus defineWorkflow.
  • 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

CommandPurposeNotes
temporal-bun initScaffold a worker + Docker assetsHonors --force to overwrite existing files.
temporal-bun doctorLoad config, emit log + metrics, verify TLSAccepts --log-format, --metrics, --metrics-exporter, --metrics-endpoint.
temporal-bun docker-buildBuild worker imageMirrors docker build -t <tag> -f <file> <context>.
temporal-bun replayDiff workflow determinism from JSON or live historiesSupports --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.