Date: 2026-01-01
Status: Draft
Author: Sean + Claude
Migration from Windmill to Temporal.io for workflow orchestration, with a focus on:
- AI Agent Platform — Durable execution for LLM-based agents
- General Workflow Engine — Scheduled tasks, approvals, notifications
- Code/GitOps/IaC friendly — Code-first over UI-first
graph TB
subgraph "selfhosted-cluster repo"
TF[tf/vault/policy-temporal.tf]
ARGO_SERVER[argocd/.../temporal-server.yaml]
ARGO_CTRL[argocd/.../temporal-worker-controller.yaml]
ARGO_WORKERS[argocd/.../temporal-workers.yaml]
end
subgraph "temporal-workers repo"
COMMON[packages/common]
W_CORE[packages/worker-core]
W_TF[packages/worker-terraform]
W_LLM[packages/worker-llm]
WEBHOOK[packages/webhook-receiver]
DOCKER[docker/Dockerfile.*]
K8S_CRDS[k8s/crds/*.yaml]
end
subgraph "K8s Namespace: temporal"
SERVER[Temporal Server<br/>frontend, history, matching, web]
CTRL[Worker Controller]
DB[(PostgreSQL<br/>CNPG)]
subgraph "Managed by Controller"
WD_CORE[worker-core-v1.2.3]
WD_TF[worker-terraform-v2.0.1]
WD_LLM[worker-llm-v0.1.0]
end
WH_SVC[Webhook Receiver]
end
subgraph "External"
VAULT[Vault]
DISCORD[Discord]
GITHUB[GitHub]
GRAFANA[Grafana Stack]
end
TF --> VAULT
ARGO_SERVER --> SERVER
ARGO_CTRL --> CTRL
ARGO_WORKERS --> K8S_CRDS
K8S_CRDS --> CTRL
CTRL --> WD_CORE & WD_TF & WD_LLM
SERVER --> DB
WD_CORE & WD_TF & WD_LLM --> SERVER
WD_CORE & WD_TF & WD_LLM --> VAULT
WD_CORE & WD_TF & WD_LLM --> GRAFANA
WH_SVC --> SERVER
DISCORD --> WH_SVC
GITHUB --> WH_SVC
sequenceDiagram
participant DEV as Developer
participant GH as GitHub Actions
participant GHCR as GHCR
participant ARGO as ArgoCD
participant CTRL as Worker Controller
participant K8S as K8s Deployments
DEV->>GH: Push to temporal-workers
GH->>GH: Build worker images (matrix)
GH->>GHCR: Push images with version tags
ARGO->>ARGO: Detect CRD changes
ARGO->>K8S: Apply TemporalWorkerDeployment CRDs
CTRL->>GHCR: Detect new image version
CTRL->>K8S: Create versioned Deployment
CTRL->>CTRL: Progressive rollout (10% → 50% → 100%)
CTRL->>K8S: Drain and delete old versions
│ └── policy-temporal.tf # Vault policies only
├── argocd/cluster-app/templates/
│ ├── temporal-server.yaml # Helm chart for Temporal server
│ ├── temporal-worker-controller.yaml # Helm chart for controller
│ └── temporal-workers.yaml # Points to worker CRDs
│ ├── database.yaml # CNPG Database resource
│ ├── secrets.yaml # ExternalSecrets
└── secrets.yaml # ExternalSecrets for workers
fzymgc-house/temporal-workers/
├── pyproject.toml # uv workspace root
│ ├── common/ # Shared utilities
│ │ └── src/temporal_common/
│ │ ├── settings.py # Pydantic settings
│ │ ├── client.py # Temporal client factory
│ │ ├── observability.py # OTel setup
│ │ └── vault.py # hvac client
│ ├── worker-core/ # Discord, GitHub, S3
│ ├── worker-terraform/ # Terraform operations
│ │ └── src/worker_terraform/
│ ├── worker-llm/ # LLM/Agent (future)
│ └── webhook-receiver/ # FastAPI webhook service
│ └── src/webhook_receiver/
│ ├── Dockerfile.terraform
│ └── serviceaccount.yaml
│ ├── worker-terraform.yaml
stateDiagram-v2
[*] --> TerraformPlan
TerraformPlan --> NotifyPlanReady: Plan succeeded
TerraformPlan --> NotifyPlanFailed: Plan failed
NotifyPlanFailed --> [*]
NotifyPlanReady --> WaitForApproval
WaitForApproval --> Approved: 👍 reaction
WaitForApproval --> Rejected: 👎 reaction
WaitForApproval --> TimedOut: 24h timeout
Rejected --> NotifyRejected
NotifyRejected --> [*]
TimedOut --> NotifyTimeout
NotifyTimeout --> [*]
Approved --> TerraformApply
TerraformApply --> NotifyComplete: Apply succeeded
TerraformApply --> NotifyApplyFailed: Apply failed
NotifyComplete --> [*]
NotifyApplyFailed --> [*]
sequenceDiagram
participant WF as Workflow
participant ACT as Activity
participant DISCORD as Discord
participant USER as User
participant WH as Webhook Receiver
WF->>ACT: terraform_plan()
ACT-->>WF: plan_output
WF->>ACT: notify_discord(plan_summary)
ACT->>DISCORD: Send embed with 👍/👎 prompt
WF->>WF: workflow.wait_condition(signal or timeout)
USER->>DISCORD: Reacts with 👍
DISCORD->>WH: Interaction webhook
WH->>WF: Signal: approval
WF->>ACT: terraform_apply()
ACT-->>WF: apply_result
WF->>ACT: notify_discord(success)
| Activity | Description | Retry Policy |
|---|
terraform_plan | Execute terraform plan, capture output | 3 attempts, exponential backoff |
terraform_apply | Execute terraform apply | 1 attempt (no retry) |
notify_discord | Send message/embed to Discord channel | 5 attempts |
| Signal | Source | Payload |
|---|
approval | Webhook receiver | {"user": "...", "timestamp": "..."} |
rejection | Webhook receiver | {"user": "...", "reason": "..."} |
graph LR
subgraph "Worker Process"
SDK[OTel SDK]
TRACES[Traces]
METRICS[Metrics]
LOGS[Logs]
end
subgraph "Collection"
COLLECTOR[OTel Collector]
end
subgraph "Grafana Stack"
TEMPO[Tempo]
PROM[Prometheus]
LOKI[Loki]
GRAFANA[Grafana]
end
SDK --> TRACES & METRICS & LOGS
TRACES & METRICS & LOGS --> COLLECTOR
COLLECTOR -->|traces| TEMPO
COLLECTOR -->|metrics| PROM
COLLECTOR -->|logs| LOKI
TEMPO & PROM & LOKI --> GRAFANA
| Metric | Purpose |
|---|
temporal_workflow_started_total | Workflow start rate |
temporal_workflow_completed_total | Success/failure rate |
temporal_activity_duration_seconds | Activity latency histogram |
llm_request_duration_seconds | LLM API call latency (future) |
llm_tokens_total | Token consumption (future) |
graph TB
subgraph "Static Secrets (startup)"
VAULT_STATIC[Vault]
ESO[External Secrets Operator]
K8S_SECRET[K8s Secret]
ENV[Environment Variables]
end
subgraph "Dynamic Secrets (runtime)"
VAULT_DYN[Vault]
HVAC[hvac client]
ACTIVITY[Activity code]
end
VAULT_STATIC --> ESO --> K8S_SECRET --> ENV
VAULT_DYN --> HVAC --> ACTIVITY
| Type | Use Case | Mechanism |
|---|
| Static | Discord token, DB creds, OTel endpoint | ExternalSecrets → env vars |
| Dynamic | Per-workflow secrets, rotated credentials | Vault SDK (hvac) in activity |
gantt
title Phase 1: Foundation
dateFormat YYYY-MM-DD
section Repos
Create temporal-workers repo :p1a, 2026-01-02, 1d
Initialize uv workspace :p1b, after p1a, 1d
Add common package :p1c, after p1b, 1d
section Infrastructure
CNPG Database manifest :p1d, 2026-01-02, 1d
Temporal server ArgoCD app :p1e, after p1d, 1d
Worker Controller ArgoCD app :p1f, after p1e, 1d
Vault policies :p1g, after p1f, 1d
- worker-core package (Discord activity)
- TemporalWorkerDeployment CRD
- Validate controller picks up image
- Hello-world workflow via CLI
- worker-terraform package
- TerraformDeploy workflow
- Discord approval flow
- Webhook receiver deployment
- Schedule creation
- Grafana dashboards
- Alerting rules
- Documentation
- Windmill deprecation
- worker-llm package
- Anthropic activity
- Tool execution activity
- Agent orchestration workflow
| Decision | Choice | Rationale |
|---|
| Temporal namespace | workflows | Descriptive, K8s ns remains temporal |
| Worker repo | Separate repo | Different release cadence, blast radius isolation |
| Dev workflow | Local Temporal | Fast iteration, single prod namespace |
| Database | CNPG main cluster | Consistent with existing pattern |
| Secrets | Hybrid (ESO + hvac) | Static at startup, dynamic when needed |
| Observability | Full OTel | Critical for AI agent debugging |
| First approval | Discord reactions | Existing tokens, immediate value |
| Deployment | Worker Controller | Rainbow deploys, graceful drain |
- Worker Controller version — Which Helm chart version to start with?
- OTel Collector — Use existing or deploy dedicated for Temporal?
- Discord bot setup — New application or reuse existing?