This repo covers self-deploying Temporal server via Docker Compose and Swarm. It serves as a reference for Docker-based deployment questions.
Running on Kubernetes instead? See temporal-helm-superchart for a self-contained Helm chart with the full observability stack, MinIO archival, and ConfigMap-based dynamic config. For this repo we use PostgreSQL for persistence for temporal db. We set up advanced visibility with Postgres DB (but options with OpenSearch / ElasticSearch are possible) for temporal_visibility. It also shows how to set up internal frontend service and use by worker service (even tho we do not set up yet custom authorizer/claims mapper). It also shows how to set up server grpc tracing with otel collector and can be visualized with Jaeger.
In addition it has a out of box sample of setting up multi cluster replication and some cli commands to get it up and running locally.
This setup uses temporal-etcd-dynconfig instead of the default file-based dynamic config client. Dynamic config values are stored in etcd and propagate to all server hosts simultaneously via etcd watch — no polling, no per-host file management.
etcd runs as a container in the compose stack. An initial set of default values is seeded from defaults.yaml on first start. You can inspect and update values at any time using etcdkeeper or etcdctl:
# Update a value — all 8 server containers pick it up within milliseconds
docker exec temporal-etcd etcdctl --endpoints=http://localhost:2379 \
put /temporal/dynamicconfig/frontend.globalNamespaceRPS -- "- value: 2000"
# List all current dynamic config values
docker exec temporal-etcd etcdctl --endpoints=http://localhost:2379 \
get /temporal/dynamicconfig/ --prefix --keys-onlySee the temporal-etcd-dynconfig repo for full documentation on key format, constraints, metrics, and multi-cluster setup.
This setup targets more of production environments as it deploys each Temporal server role (frontend, matching, history, worker) in individual containers. The setup runs two instances (containers) for frontend, matching and history hosts. Service sets up 2K shards, so ~1K per history host.
Each server role container exposes server metrics under its own port. Note that bashing into the admin-tools image also gives you access to tctl as well as the temporal-tools for different dbs.
First we need to install the loki plugin (you have to do this just one time)
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
Check if the plugin is installed:
docker plugin ls
(should see the Loki Logging Driver plugin installed
Repo contains a "poller" docker image that needs to be built the first time. This image calls DeepHealthCheck frontend api periodically which allows us to monitor our history services, and will allow us to emit the host_health metric. To build it every time start with "--build":
docker network create temporal-network
docker compose -f compose-postgres.yml -f compose-services.yml up --build --detach
If you have already built the image and you didnt change the go code in /poller dir, then just run:
docker network create temporal-network
docker compose -f compose-postgres.yml -f compose-services.yml up --detach
tctl cl h
should return "SERVING"
Bash into admin-tools container and run tctl (you can do this from your machine if you have tctl installed too)
docker container ls --format "table {{.ID}}\t{{.Image}}\t{{.Names}}"
copy the id of the temporal-admin-tools container
docker exec -it <admin tools container id> bash
tctl cl h
you should see response:
temporal.api.workflowservice.v1.WorkflowService: SERVING
Note: if you set up HAProxy instead of default Envoy, and dont see "SERVING" but rather "context deadline exceeded errors" restart the "temporal-haproxy" container. Not yet sure why but haproxy is doing something weird. Once you restart it admin-tools container should be able to finish its setup-server script and things should work fine. If anyone can figure out the issue please commit PR. Thanks.
We start postgres from a separate compose file but you don't have to and can combine them if you want.
By the way, if you want to docker exec into the postgres container do:
docker exec -it <temporal-postgres container id> psql -U temporal
\l
which should show the temporal and temporal_visiblity dbs
(You can do this via Portainer as well, this just shows the "long way")
In addition let's check out the rings in cluster:
tctl adm cl d
You should see two members for frontend, matching and history service rings. One for the worker service (typically you dont need to scale worker service)
- Frontend (via grpcurl)
grpcurl -plaintext -d '{"service": "temporal.api.workflowservice.v1.WorkflowService"}' 127.0.0.1:7233 grpc.health.v1.Health/Check
again you can just run tctl cl h too
- Frontend (via grpc-health-probe)
grpc-health-probe -addr=localhost:7233 -service=temporal.api.workflowservice.v1.WorkflowService
To check the two frontend services individually:
grpc-health-probe -addr=localhost:7236 -service=temporal.api.workflowservice.v1.WorkflowService
grpc-health-probe -addr=localhost:7237 -service=temporal.api.workflowservice.v1.WorkflowService
- Internal Frontend (via grpc-health-probe)
grpc-health-probe -addr=localhost:7233 -service=temporal.api.workflowservice.v1.WorkflowService
- Matching (via grpc-health-probe)
grpc-health-probe -addr=localhost:7235 -service=temporal.api.workflowservice.v1.MatchingService
grpc-health-probe -addr=localhost:7239 -service=temporal.api.workflowservice.v1.MatchingService
- History via grpc-health-probe)
grpc-health-probe -addr=localhost:7234 -service=temporal.api.workflowservice.v1.HistoryService
grpc-health-probe -addr=localhost:7238 -service=temporal.api.workflowservice.v1.HistoryService
Added support for HTTP api which became available since 1.22.0 server release to test you can do for example:
curl http://localhost:7243/api/v1/namespaces/default
once your service is up and running. For more info see here
Since server release 1.30 we need to fetch the embedded template and use that to display static config. It's no longer just created by dockerize in /etc/temporal/config/docker.yaml Can do something like this (change url to embedded config to reflect your server version so you get right embedded static config template)
wget -q -O /tmp/development.yaml \
https://raw.githubusercontent.com/temporalio/temporal/v1.31.0/common/config/config_template_embedded.yaml
temporal-server --config /tmp render-config > /tmp/resolved.yaml && cat /tmp/resolved.yaml
- Server metrics (raw)
- Prometheus targets (scrape points)
- Grafana (includes server, sdk, docker, and postgres dashboards)
- No login required
- In order to scrape docker system metrics add "metrics-addr":"127.0.0.1:9323" to your docker daemon.js, on Mac this is located at ~/.docker/daemon.json
- Go to "Explore" and select Loki data source to run LogQL against server metrics
- Web UI v2
- Web UI v1
- Portainer
- Note you will have to create an user the first time you log in
- Yes it forces a longer password but whatever
- Jaeger - includes server grpc traces
- PgAdmin (username: pgadmin4@pgadmin.org passwd: admin)
- etcd keeper
- minio console (username: minioadmin passwd: minioadmin)
- cAdvisor to monitor docker containers
Docker server image by default use this server config template. This is a base template that may not fit everyones needs. You an define your custom configuration template if you wish and this is what we are doing via my_config_template.yaml. With this you can customize the template as you wish, for example you could configure env vars for namespace setup, like set up s3 archival etc which is not possible with the default template.
This demo also shows how to exclude certain metrics produced by Temporal services when scraping their metric endpoints, for example here we drop all metrics for the "temporal_system" namespace.
Envoy / HAProxy / NGINX role is exposed on 127.0.0.1:7233 (so all SDK samples should work w/o changes). It is load balancing the two Temporal frontend services defined in the docker compose.
This sample uses Envoy load balancing by default. Check out the Envoy config file here and make any necessary changes. Note this is just a demo so you might want to update the values where needed for your prod env. Envoy pushes access logs to stdout and is picked up by loki, so can run all queries in Grafana. This includes grpc code and size and everything.
You can set up HAProxy load balancing if you want. It load balances our two frontend services. Check out the HAProxy config file here and make any necessary changes. Note this is just a demo so you might want to update the values where needed for your prod env.
I have ran into some issues with this HAProxy setup, specifically sometimes having to restart its container in order for admin-tools to be able to complete setup, as well as for executions first workflow task timing out. Pretty sure this has to do with some problem with the config, maybe someone could look and fix.
You can also have NGINX configured and use it for load balancing. It load balanced our two temporal frontends. Check out the NGINX config file here and make any necessary adjustments. This is just a demo remember and for production use you should make sure to update values where necessary.
docker-compose down --volumes
docker system prune -a
docker volume prune
docker compose up --detach
docker compose up --force-recreate
docker stop $(docker ps -a -q)
docker rm $(docker ps -a -q)
docker compose -f compose-postgres.yml -f compose-services.yml down --remove-orphans
docker network rm temporal-network
# remove etcd (dynamic config) volume
docker volume rm my-temporal-dockercompose_etcd-data
# restart + full rebuild of custom server image
docker compose -f compose-postgres.yml -f compose-services.yml down
docker compose -f compose-postgres.yml -f compose-services.yml build \
temporal-history temporal-history2 \
temporal-matching temporal-matching2 \
temporal-frontend temporal-frontend2 \
temporal-internal-frontend \
temporal-worker
docker compose -f compose-postgres.yml -f compose-services.yml up --detach
- "Not enough hosts to serve the request"
- Can happen on startup when some temporal service container did not start up properly, run the docker compose command again typically fixes this
Here are some extra configurations, try them out and please report any errors.
Dual visibility allows you to configure a secondary visibility store. One use case of having dual visibility is if you need to migrate from one store to another or switch to secondary store in case of failures on primary one. This is typically what you want to do in a production env.
In this sample we set up dual visibility for our SQL visibility setup (Postgres). Please note that we set this up on the same db instance. This demo uses a single postgres to set up all dbs, primary, visibility, and secondary visibility. For a prod env you might want to separate these to 3 completely separate envs (recommended).
The key dynamic config setting for dual visibility is to enable writes to both primary and secondary vis:
system.secondaryVisibilityWritingMode:
- value: "dual"
constraints: {}
system.enableReadFromSecondaryVisibility:
- value: false
constraints: {}
This sets up Temporal to write visibility data to both primary and secondary vis stores. Let's say you run into issues on this primary store, via dynamic config again you can switch read to secondary
system.enableReadFromSecondaryVisibility:
- value: true
constraints: {}
If you experience complete outage of primary vis store, you can change your static config as needed and then again look at your dynamic config to write to primary and-or secondary as again needed.
Covers: start two clusters → connect them → promote a namespace to global → backfill existing executions → failover to c2 → decommission c1.
1. Clear your Docker environment (see Some useful Docker commands)
2. Create the network and start both clusters
docker network create temporal-network-replication
docker compose -f compose-services-replication.yml up --detach3. Verify both clusters are healthy
temporal --address 127.0.0.1:7233 operator cluster health
temporal --address 127.0.0.1:2233 operator cluster health4. Confirm cluster names
temporal --address 127.0.0.1:7233 operator cluster describe -o json | jq .clusterName
# expected: "c1"
temporal --address 127.0.0.1:2233 operator cluster describe -o json | jq .clusterName
# expected: "c2"5. Get container IPs — needed for the upsert commands in Phase 3
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' temporalc1
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' temporalc2Note these as TEMPORALC1_IP and TEMPORALC2_IP.
6. Create the test namespace on c1
temporal --address 127.0.0.1:7233 operator namespace create replicationtest7. Start sample workflows
Run a mix of short (completing) and long-running workflows so you can see both completed and running executions replicate. This Java sample creates 30 executions — 20 complete, 10 remain running:
https://gist.github.com/tsurdilo/f0ef3ea2940e877aaec7489370ae099c
8. Verify workflows are on c1
http://localhost:8081/namespaces/replicationtest/workflows
9. Confirm namespace does not exist on c2 yet
http://localhost:8082/namespaces/replicationtest/workflows
Each cluster stores its own peer registry locally — both directions must be run independently.
10. Register c2 as a peer on c1
temporal --address 127.0.0.1:7233 operator cluster upsert \
--enable-connection \
--enable-replication \
--frontend-address "TEMPORALC2_IP:2233"11. Register c1 as a peer on c2
temporal --address 127.0.0.1:2233 operator cluster upsert \
--enable-connection \
--enable-replication \
--frontend-address "TEMPORALC1_IP:7233"12. Verify both clusters see each other
temporal --address 127.0.0.1:7233 operator cluster list
temporal --address 127.0.0.1:2233 operator cluster list13. Promote replicationtest from local to global namespace
temporal --address 127.0.0.1:7233 operator namespace update \
--namespace replicationtest \
--promote-global14. Verify it is now a global namespace
temporal --address 127.0.0.1:7233 operator namespace describe \
--namespace replicationtest -o json | jq .isGlobalNamespace
# expected: true15. Add both clusters to the namespace replication config
temporal --address 127.0.0.1:7233 operator namespace update \
--namespace replicationtest \
--cluster c1 \
--cluster c216. Verify replication config shows both clusters
temporal --address 127.0.0.1:7233 operator namespace describe \
--namespace replicationtest -o json | jq .replicationConfig17. Confirm namespace now exists on c2
http://localhost:8082/namespaces/replicationtest/workflows
No workflows yet — the namespace was replicated but existing executions are not backfilled automatically.
New executions started after step 15 replicate automatically. The 30 existing executions on c1 need to be force-replicated.
18. Start the force-replication system workflow on c1
temporal --address 127.0.0.1:7233 workflow start \
--namespace temporal-system \
--type force-replication \
--task-queue default-worker-tq \
--input '{"Namespace": "replicationtest", "ConcurrentActivityCount": 4, "OverallRps": 80}'19. Monitor until complete
20. Verify all executions are on c2
http://localhost:8082/namespaces/replicationtest/workflows
Expected: 20 completed and 10 running executions.
namespace-handover is a safe failover — it waits for replication lag to drain before flipping the active cluster, unlike a direct namespace update.
21. Run the namespace-handover system workflow on c1
temporal --address 127.0.0.1:7233 workflow start \
--namespace temporal-system \
--task-queue default-worker-tq \
--type namespace-handover \
--input '{"Namespace": "replicationtest", "RemoteCluster": "c2", "AllowedLaggingSeconds": 120, "HandoverTimeoutSeconds": 5}'22. Verify active cluster is now c2
temporal --address 127.0.0.1:7233 operator namespace describe replicationtest \
-o json | jq .replicationConfig.activeClusterName
# expected: "c2"Note: If this still shows
"c1"immediately after the handover workflow completes, wait up to 60 seconds and retry. The namespace registry on each server polls for changes on a 60s interval (dynamicConfigClient.pollInterval). The handover itself is done — the cache just hasn't refreshed yet.
23. Switch clients and workers to c2
Point your SDK workers and clients at 127.0.0.1:2233. Both clusters have dcRedirectionPolicy: all-apis-forwarding, so signals and starts sent to c1 will continue to be forwarded to c2 in the interim — but workers polling c1 will also be forwarded. Stop c1 workers before or immediately after switching DNS/addresses to avoid c1 workers picking up tasks meant for c2.
Migration vs long-running dual-cluster: This walkthrough follows the migration/decommission path — c1 is being torn down after failover, so poll forwarding from c1 to c2 is harmless for the short window before shutdown. If instead you intend to keep both clusters running long-term (so the namespace can fail back to c1 in the future), do not proceed to Phase 7. Instead, enforce standby worker isolation on c1 by adding one of the following to
dynamicconfig/development-c1.yaml:# Option 1 — no forwarding at all from c1 (clients hitting c1 get NamespaceNotActive) system.enableNamespaceNotActiveAutoForwarding: - value: false constraints: namespace: replicationtest # Option 2 — forward writes (signals, starts) but never polls from c1 system.forceNamespaceSelectedAPIAutoForwarding: - value: true constraints: namespace: replicationtestAfter a fail-back to c1, apply the same change on c2 and remove it from c1.
24. Remove c1 from the namespace replication config
temporal --address 127.0.0.1:2233 operator namespace update \
--namespace replicationtest \
--cluster c225. Verify only c2 remains in replication config
temporal --address 127.0.0.1:2233 operator namespace describe \
--namespace replicationtest -o json | jq .replicationConfig26. Delete the namespace on c1
temporal --address 127.0.0.1:7233 operator namespace delete \
--namespace replicationtest27. Verify namespace is gone from c1, still present on c2
- http://localhost:8081/namespaces/replicationtest — should 404
- http://localhost:8082/namespaces/replicationtest — should load
28. Disconnect the clusters
temporal --address 127.0.0.1:7233 operator cluster remove --name c2
temporal --address 127.0.0.1:2233 operator cluster remove --name c129. Verify c2 no longer references c1
temporal --address 127.0.0.1:2233 operator cluster describe -o json30. Complete running executions on c2
If you used the Java sample from Phase 2, run the worker pointed at c2 to pick up and complete the 10 running executions:
https://gist.github.com/tsurdilo/4114521b617016b5a5872ebf50e1494b
c1 can now be decommissioned.
This setup runs a custom-built Temporal server image from local source (~/devel/temporal/temporal). Upgrading means updating that source checkout to the target version, aligning the admin-tools image, running any schema migrations, and rebuilding.
- Persistence:
postgres12 - Visibility:
postgres12 - Docker network:
temporal-network - PostgreSQL container:
temporal-postgresql - PostgreSQL user:
temporal, password:temporal - Server image: custom-built from
~/devel/temporal/temporalsource - Admin tools image:
temporalio/admin-tools:<version>(set viaTEMPORAL_ADMINTOOLS_IMGin.env)
docker compose -f compose-postgres.yml -f compose-services.yml down --remove-orphansCheck out the target release tag in the Temporal server source:
cd ~/devel/temporal/temporal
git fetch --tags
git checkout v1.32.0 # replace with target versionIn .env, set TEMPORAL_ADMINTOOLS_IMG to the target version:
TEMPORAL_ADMINTOOLS_IMG=1.32.0
The admin-tools version must match the server source version so schema migration scripts are aligned.
docker exec -it temporal-postgresql psql -U temporal -d temporal -c "SELECT curr_version FROM schema_version;"
docker exec -it temporal-postgresql psql -U temporal -d temporal_visibility -c "SELECT curr_version FROM schema_version;"Note both versions — these are your baseline before any migration.
Compare your current schema versions (from Step 4) against the highest version available in the target admin-tools image:
docker run --rm temporalio/admin-tools:1.32.0 \
ls /etc/temporal/schema/postgresql/v12/temporal/versioned
docker run --rm temporalio/admin-tools:1.32.0 \
ls /etc/temporal/schema/postgresql/v12/visibility/versionedIf your current schema version is already at the highest version listed — no migration needed, skip to Step 7. If higher versions are present — proceed to Step 6.
Note: The
lsoutput sorts lexicographically, not numerically — sov1.19appears betweenv1.18andv1.2in the listing. Find the highest numeric version manually, do not rely on the last entry in the listing.
Schema must be updated before rolling the binary. Use the target version admin-tools image:
# Primary DB
docker run --rm \
--network temporal-network \
-e SQL_PASSWORD=temporal \
temporalio/admin-tools:1.32.0 \
temporal-sql-tool \
--plugin postgres12 \
--ep postgresql \
-u temporal \
-p 5432 \
--db temporal \
update-schema \
--schema-dir /etc/temporal/schema/postgresql/v12/temporal/versioned
# Visibility DB
docker run --rm \
--network temporal-network \
-e SQL_PASSWORD=temporal \
temporalio/admin-tools:1.32.0 \
temporal-sql-tool \
--plugin postgres12 \
--ep postgresql \
-u temporal \
-p 5432 \
--db temporal_visibility \
update-schema \
--schema-dir /etc/temporal/schema/postgresql/v12/visibility/versionedRebuild the server image from the updated source and restart all services:
docker compose -f compose-services.yml up --build -dDocker Compose detects the build: stanza on the Temporal service containers and rebuilds from ~/devel as the build context. PostgreSQL is left running.
temporal operator cluster describe -o jsonConfirm serverVersion shows the target version.
Then verify both DBs are at the expected schema versions:
docker exec -it temporal-postgresql psql -U temporal -d temporal -c "SELECT curr_version FROM schema_version;"
docker exec -it temporal-postgresql psql -U temporal -d temporal_visibility -c "SELECT curr_version FROM schema_version;"The custom server in server/ includes a frontend gRPC interceptor that detects unencrypted payloads flowing through the Temporal frontend. It is observe-only — every request is always allowed through.
What it does: when any frontend API call carries a payload whose encoding metadata is json/plain or binary/plain (i.e., no payload codec is in use), the interceptor:
- Increments a Prometheus counter
plaintext_payload_detected_totaltagged withnamespace,operation,encoding, and where availableworkflowTypeandtaskqueue. Thepayload_fieldtag identifies which field inside the request held the unencrypted payload — e.g.inputon aStartWorkflowExecutionmeans workflow input args were unencrypted, whileScheduleActivityTaskon aRespondWorkflowTaskCompletedmeans activity input inside a worker command was unencrypted. This distinction matters because client-side and worker-side unencrypted traffic require different fixes from the tenant. - Emits a structured
WARNlog line with the same fields.
This gives cluster operators visibility into which tenants and workflow types are still sending unencrypted data, without breaking any existing traffic. The intended use is to run it in observe-only mode, alert on the metric, give tenants time to deploy a payload codec, then optionally harden it into a blocking gate.
Implementation: server/interceptors/plaintext_payload.go
Wiring into the server: server/main.go — the interceptor is instantiated after the shared metrics handler is created and passed to temporal.WithChainedFrontendGrpcInterceptors:
plainTextInterceptor := interceptors.NewPlainTextPayloadInterceptor(logger, metricsHandler)
temporal.NewServer(
// ...
temporal.WithChainedFrontendGrpcInterceptors(plainTextInterceptor.Intercept),
)Running the tests (requires the local go.work in server/ which redirects the Docker module paths to your local checkouts):
cd server && go test ./interceptors/ -vSee server/interceptors/README.md for the full list of covered APIs, all metric tag names, PromQL queries for Grafana, and instructions for extending it to block requests.
The custom server in server/ includes a status-filtered archival provider backed by MinIO — an S3-compatible object store that runs as a container in the compose stack. Both workflow history and visibility records are archived to MinIO, gzip-compressed, when a workflow closes.
Archival is enabled by default (USE_MINIO_ARCHIVAL=true in .env). To disable it, set USE_MINIO_ARCHIVAL=false before starting the stack.
The provider is wired into the server via temporal.WithCustomHistoryArchiverFactory and temporal.WithCustomVisibilityArchiverFactory in server/main.go. It registers the minio:// URI scheme. The server config template at template/my_config_template.yaml conditionally enables the archival block when USE_MINIO_ARCHIVAL is set; TEMPORAL_SERVER_CONFIG_FILE_PATH in compose-services.yml tells each server container to load this template instead of the compiled-in default.
On first start, minio-init creates two buckets automatically:
temporal-history— one object per workflow execution (key:history/{namespaceID}/{workflowID}/{runID}_{failoverVersion}.history.gz)temporal-visibility— one object per execution, date-partitioned by close time (key:visibility/{namespaceID}/{YYYY}/{MM}/{DD}/{closeTimeNano}_{shortRunID}.visibility.gz)
The MinIO console (credentials: minioadmin / minioadmin) lets you browse both buckets.
The namespaceDefaults.archival block in the server config is applied only at namespace creation time — it has no effect on existing namespaces. Once a namespace exists, its archival state (HistoryArchivalState, VisibilityArchivalState, and the URIs) is stored in the database and loaded from there on every startup. Static config is never consulted again for existing namespaces.
What this means in practice:
- Fresh start (empty DB):
temporal-admin-toolscreates thedefaultnamespace after the server is up. BecausenamespaceDefaults.archivalin the rendered config hasstate: enabledwith the minio URIs, the namespace is created with archival already enabled. - Restart with a persistent DB: the namespace DB record already has archival enabled from its original creation — the server loads it from DB as-is, no extra step.
- Namespace created before archival was enabled: the DB record has
HistoryArchivalState: disabled. Restarting withUSE_MINIO_ARCHIVAL=truedoes not update it.temporal-admin-toolshandles this by running the following update on every start whenUSE_MINIO_ARCHIVAL=true(seescript/setup.sh):
temporal operator namespace update -n default \
--history-archival-state enabled \
--history-uri "minio://temporal-history/history" \
--visibility-archival-state enabled \
--visibility-uri "minio://temporal-visibility/visibility"If archival is not showing as enabled after the stack comes up, verify the namespace state and run the command above manually if needed:
temporal operator namespace describe -n default | grep -i archival# list all archived workflows
temporal workflow list --archived -n default
# filter by execution status
temporal workflow list --archived -n default --query "ExecutionStatus = 'Completed'"
temporal workflow list --archived -n default --query "ExecutionStatus = 'Failed'"
temporal workflow list --archived -n default --query "ExecutionStatus = 'Terminated'"
temporal workflow list --archived -n default --query "ExecutionStatus = 'TimedOut'"
temporal workflow list --archived -n default --query "ExecutionStatus = 'Canceled'"
temporal workflow list --archived -n default --query "ExecutionStatus = 'ContinuedAsNew'"
# retrieve full event history of an archived workflow
temporal workflow show -n default --workflow-id <id> --run-id <run-id>Decompression is transparent — the CLI, SDK, and UI all receive normal uncompressed responses.
Edit the allowedStatuses list in template/my_config_template.yaml under archival.history.provider.customStores.minio and the matching visibility block. An empty list ([]) archives all terminal statuses. Add specific values (e.g. "Failed", "Terminated") to restrict archival to only those statuses.
For full implementation details, object key layout, known limitations, and instructions for testing archival locally with reduced delays, see server/archiver/README.md.