Skip to main content

Temporal

Durable workflow orchestration engine

CategoryIntegration
Deploy./uis deploy temporal
Undeploy./uis undeploy temporal
Depends onpostgresql
Required byNone
Helm charttemporal/temporal (pinned to 1.6.0 — server 1.31.2, UI 2.52.0)
Default namespacetemporal

What It Does

Temporal is a durable execution platform. You write workflows as ordinary code — Go, TypeScript, Python, Java, .NET or PHP — and Temporal records every state transition in a database. If the process running your workflow crashes, gets redeployed, or waits three days for a human approval, the workflow resumes exactly where it left off with its local variables intact.

Key capabilities:

  • Durable workflows — code survives crashes, restarts, and deploys; no saga/state-machine boilerplate
  • Automatic retries — activity retry policies, timeouts, and backoff declared in code
  • Long timerssleep(30 days) is a normal thing to write
  • Visibility — list and query running/completed workflows from the Web UI or the CLI
  • Web UI at temporal.localhost — inspect workflow histories, terminate, reset, replay

Typical uses: order/payment pipelines, user onboarding flows, data pipelines with retries, scheduled jobs that must not be lost, anything currently held together by a cron job plus a status column.

PostgreSQL-only — no Elasticsearch, no Cassandra

Temporal needs two datastores: history (workflow state) and visibility (the searchable index behind workflow list and the UI). Many Temporal deployments put visibility in Elasticsearch. UIS does not — since Temporal 1.20, advanced visibility works on PostgreSQL 12+, so both stores live in the shared UIS PostgreSQL:

StoreDatabasePurpose
History (default)temporalWorkflow state and event history
Visibilitytemporal_visibilitySearchable workflow index

That keeps a laptop cluster small: no Elasticsearch, no Cassandra, and no second PostgreSQL. The setup playbook creates the temporal role and both databases inside the existing postgresql service in the default namespace, then the chart's schema Job applies the Temporal SQL schema.

Deploy

./uis deploy postgresql   # dependency
./uis deploy temporal

Access after deploy:

WhatURL
Web UI (browser)http://temporal.localhost
HTTP API (browser / apps outside the cluster)http://temporal-api.localhost/api/v1/namespaces
gRPC frontend (from inside the cluster)temporal-frontend.temporal.svc.cluster.local:7233
gRPC frontend (from the host machine)./uis expose temporallocalhost:37233

The Temporal namespace default (3-day retention) is created at deploy time, so a worker can connect immediately.

Verify

# Full E2E test suite (health, datastores, HTTP API, workflow round-trip, UI, routing)
./uis verify temporal

# Manual checks
kubectl get pods -n temporal
kubectl exec -n temporal deploy/temporal-admintools -- temporal operator cluster health
kubectl exec -n temporal deploy/temporal-admintools -- temporal workflow list

Configuration

Temporal configuration is in manifests/086-temporal-config.yaml. Key settings:

SettingValueNotes
History storepostgres12temporalShared postgresql.default service
Visibility storepostgres12temporal_visibilityAdvanced visibility on PostgreSQL, no Elasticsearch
numHistoryShards4Cannot be changed after the first deploy without wiping the temporal database. Matches the temporalio/auto-setup default; the chart default of 512 is a production number.
FrontendgRPC 7233, HTTP 7243Service temporal-frontend
Web UI8080Service temporal-web
Replicas1 per componentfrontend, history, matching, worker, web, admintools
Temporal namespacedefault, 3d retentionCreated by the chart's namespace Job

Components

PodRole
temporal-frontendgRPC/HTTP API — clients and workers connect here
temporal-historyOwns workflow history shards
temporal-matchingTask queue matching
temporal-workerTemporal's own internal system workers
temporal-webWeb UI
temporal-admintoolstemporal / tctl CLIs, used by ./uis verify temporal

Secrets

VariableFilePurpose
TEMPORAL_POSTGRES_USER.uis.secrets/secrets-config/00-common-values.env.templatePostgreSQL role Temporal logs in as
TEMPORAL_POSTGRES_PASSWORDsame filePassword for that role
TEMPORAL_POSTGRES_DATABASEsame fileHistory database name (temporal)
TEMPORAL_POSTGRES_VISIBILITY_DATABASEsame fileVisibility database name (temporal_visibility)

They land in the urbalurba-secrets Secret in the temporal namespace, together with TEMPORAL_ADDRESS and TEMPORAL_NAMESPACE for applications. The Helm chart reads the password directly out of that Secret (existingSecret / secretKey), so no credential is ever written into a manifest.

Read them back with:

kubectl get secret urbalurba-secrets -n temporal -o jsonpath='{.data.TEMPORAL_POSTGRES_USER}' | base64 -d
kubectl get secret urbalurba-secrets -n temporal -o jsonpath='{.data.TEMPORAL_ADDRESS}' | base64 -d

To change them: edit 00-common-values.env.template, then ./uis secrets generate, ./uis secrets apply, and re-deploy Temporal (the setup playbook rotates the PostgreSQL role password to match).

Connecting a worker

Workers running in the cluster mount urbalurba-secrets from the temporal namespace, or hard-code the in-cluster address:

env:
- name: TEMPORAL_ADDRESS
value: temporal-frontend.temporal.svc.cluster.local:7233
- name: TEMPORAL_NAMESPACE
value: default

Example (TypeScript SDK):

import { NativeConnection, Worker } from '@temporalio/worker';

const connection = await NativeConnection.connect({
address: process.env.TEMPORAL_ADDRESS, // temporal-frontend.temporal.svc.cluster.local:7233
});

const worker = await Worker.create({
connection,
namespace: process.env.TEMPORAL_NAMESPACE ?? 'default',
taskQueue: 'my-queue',
workflowsPath: require.resolve('./workflows'),
activities,
});

await worker.run();

For development from the host machine (or a devcontainer), port-forward the gRPC frontend:

./uis expose temporal         # binds localhost:37233 -> svc/temporal-frontend:7233
./uis expose temporal --stop

Key Files

FilePurpose
manifests/086-temporal-config.yamlHelm values (PostgreSQL-only persistence, resources, components)
manifests/087-temporal-ingressroute.yamlTraefik routes: temporal.* → Web UI, temporal-api.* → HTTP API
ansible/playbooks/086-setup-temporal.ymlDeployment playbook (creates role + databases, installs chart, health checks)
ansible/playbooks/086-remove-temporal.ymlRemoval playbook (keeps the databases unless --purge)
ansible/playbooks/086-test-temporal.ymlE2E verification playbook (./uis verify temporal)

Undeploy

# Removes the deployment, keeps the temporal and temporal_visibility databases
./uis undeploy temporal

# Drops both databases and the temporal role — all workflow history is gone permanently
./uis undeploy temporal --purge

Temporal has no PVC of its own in UIS: all of its state lives in the shared PostgreSQL. "Keeping the data" therefore means leaving those two databases alone.

Troubleshooting

Deploy fails with "TEMPORAL_POSTGRES_PASSWORD not found":

The secret has not been applied to the cluster yet:

./uis secrets generate
./uis secrets apply
./uis deploy temporal

If secrets generate does not produce the key, your .uis.secrets/secrets-config/00-common-values.env.template predates Temporal — copy the TEMPORAL_* block from the shipped template into it.

Deploy fails with "PostgreSQL is required for Temporal":

./uis deploy postgresql
./uis deploy temporal

Helm install times out on the schema hook:

The chart runs temporal-sql-tool setup-schema as a pre-install Job. Look at it:

kubectl get jobs -n temporal
kubectl logs -n temporal job/temporal-schema --all-containers

A permission error here usually means the temporal role cannot create tables in public — re-running ./uis deploy temporal re-applies the GRANT ALL ON SCHEMA public step.

Pods restart with "unable to connect to database":

The role password in PostgreSQL and the one in urbalurba-secrets have drifted (usually after a secret rotation without a re-deploy). Fix with ./uis secrets apply followed by ./uis deploy temporal — the playbook runs ALTER ROLE … WITH PASSWORD to bring them back in sync.

temporal workflow list returns nothing but workflows exist:

That is the visibility store, not the history store. Check that the temporal_visibility schema was applied (kubectl get jobs -n temporal) and that the config still names both stores:

kubectl get cm temporal-config -n temporal -o jsonpath='{.data.config_template\.yaml}' | head -30

Web UI loads but shows "Unable to connect":

The UI talks to temporal-frontend:7233. Check the frontend is Ready and SERVING:

kubectl get pods -n temporal -l app.kubernetes.io/component=frontend
kubectl exec -n temporal deploy/temporal-admintools -- temporal operator cluster health

Learn More