Lessons
16 — Temporal / Argo Workflows

Lesson 16 — Workflow orchestration: Temporal & Argo Workflows

2026-09-24

What interviewers are really probing

Lesson 15 gated who may change the substrate. This lesson is about what runs after the change — multi-hour cell upgrades, drain waves, ML checkpoint → validate → promote, inference canaries that wait on humans or metrics. Interviewers are not asking “can you write a CronJob.” They want evidence you pick the right orchestration plane and keep it from becoming a blast-radius machine:

  • Temporal vs Argo — when durable code workflows win vs when K8s-native DAG pods win (and how they compose)
  • Determinism & history — Temporal workflow code that survives replay; Continue-As-New before history melts
  • Task queues / workers — multi-cluster workers, sticky execution, starvation vs retry storms
  • Argo CR surface — Workflow / CronWorkflow / WorkflowTemplate, DAG vs steps, artifacts, pod GC
  • Fleet ops — upgrade orchestration, drain waves, canary across cells (echoes L3 / L4)
  • ML pipelines — GPU train jobs as Argo steps; Temporal signals for promote/rollback; weights & signed images (L12–L14)
  • Security — Temporal mTLS + payload encryption; Argo SA least privilege; private artifact buckets; no world-readable checkpoints

Interview sentence: “Atlantis applies the cell change; Temporal owns the durable multi-step saga (drain waves, canary signals, rollback); Argo runs the K8s-native GPU/batch DAGs with signed images and scoped weight IAM — workers and workflow SAs are never cluster-admin, and artifacts are never world-readable.”

Core diagram: Temporal + Argo composing for fleet / ML

Temporal and Argo Workflows at fleet scale: Temporal client to frontend to history/matching to workers; Argo controller to Workflow CR to pods to artifact store; compose strip after Atlantis

Read it top-down: An Atlantis apply (L15) lands a cell or ML-platform change → a Temporal client starts (or signals) a durable workflow through the Frontend (mTLS) → History persists every event; Matching dispatches work on task queues to workers that may live in many clusters → for K8s-native GPU/batch steps the Temporal activity (or a Schedule) submits an Argo Workflow CR → the workflow-controller spawns step pods that pull signed images, write artifacts to a private bucket, and optionally pull weights with scoped IAM (L13) → Temporal signals gate canary promote/rollback across fleet cells. The PNG compose strip is the interview story: IaC → durable saga → DAG pods → artifacts → canary.

Idiosyncrasies / operator depth

When Temporal wins vs when Argo wins

PlaneWins whenFootgun if forced
TemporalLong-running durable business/infra logic: retries, timers (hours–days), signals, saga/compensation, multi-cluster workers, human-in-the-loopModeling every YAML DAG as Temporal activities that just kubectl apply — you reinvent Argo poorly
Argo WorkflowsK8s-native DAGs, CI-like pipelines, GPU training/batch as pods, artifact plumbing (S3/GCS), CronWorkflow fan-outEncoding multi-day business saga with signals/compensation purely as Workflow CRs — history of CRs and stuck Pending become the product
ComposeTemporal orchestrates when/whether; Argo executes pod DAGsTwo uncoordinated control planes racing the same Deployment

At hyperscale: Temporal = durable control brain; Argo = K8s execution muscle. Same instinct as L4 (scheduler vs executor separation).

Temporal: workflow vs activity determinism

  • Workflow code must be deterministic — given the same history, replay produces the same commands. No random, no local clock (workflow.Now), no direct I/O, no unordered map iteration surprises in some languages.
  • Activities do the real world: HTTP, cloud APIs, kubectl, GPU job submit, bucket GetObject. Retries, timeouts, heartbeats live here.
  • Nondeterminism error after a deploy = you changed workflow code without versioning (GetVersion / worker versioning) while open executions still replay old history.
  • Sticky execution caches the workflow worker to avoid full replay every task — great until the sticky worker dies and everyone pays replay cost at once.

Task queues, workers, namespaces

KnobOperator meaning
Task queueRouting key: fleet-ops, ml-train-gpu, cell-us-east-1-a — workers poll what they can run
Worker identityHorizontal scale by adding pollers; starvation = not enough workers or activities stuck without heartbeat
NamespaceBlast-radius + tenancy boundary (team/env); separate retention, search attributes, auth
Visibility / search attributescell_id, pipeline, change_ticket — how oncall finds “all drains for cell-a”
ScheduleCron-like durable schedules (prefer over homemade CronJobs that fire StartWorkflow without visibility)

History size, Continue-As-New, versioning

  • Event history is the source of truth. Unbounded loops (poll forever, append forever) → history bloat → latency and Workflow history size exceeds limit.
  • Continue-As-New checkpoints into a fresh run with carry-over state — mandatory for long-lived fleet controllers and chatty ML promotion loops.
  • Worker versioning / GetVersion — ship new workflow logic without breaking open executions. Treat workflow code like a schema.

Argo: Workflow / CronWorkflow / WorkflowTemplate

KindUse
WorkflowOne-shot DAG or steps run
CronWorkflowScheduled (drains, nightly eval, drift-adjacent batch) — still a Workflow under the hood
WorkflowTemplate / ClusterWorkflowTemplateReuse; pin versions like modules in L15

DAG vs steps: DAG expresses dependencies explicitly (depends); steps are sequential lists with fan-out via withItems. For ML “train → validate → promote”, DAG with explicit fail paths + exit handlers beats a fragile linear steps list.

Parallelism, retries, deadlines, artifacts

  • parallelism, semaphore/mutex — cap concurrent GPU pods or external API calls; without them CronWorkflow + retryStrategy = retry storm.
  • retryStrategy + activeDeadlineSeconds — retries without a deadline are how zombie GPU jobs burn quota overnight.
  • Artifacts (S3/GCS) — outputs between steps; misconfigured IAM → Pending forever or world-readable checkpoints (security incident, not a “storage glitch”).
  • archiveLogs / pod GC — keep enough for forensics; TTL aggressively or etcd/API and disks drown.
  • Resource templates / executor plugins — call K8s resources or plugins; still subject to admission (L11 / L14).

Fleet ops patterns (the JD meat)

PatternTemporal roleArgo role
Cluster upgrade wavesSaga: cordon → drain → wait healthy → next cell; signal pause on SLO burnOptional per-node DaemonSet/job steps
Node drain wavesDurable progress across hours; compensation if drain stuckPod-disruption-aware jobs; PDB-respecting batch
Canary across cellsSignal canary_ok / rollback from metrics or humanRollout jobs / eval Workflows per cell
ML train → validate → promoteOuter workflow: gate promote on metrics + signed digestInner DAG: GPU train, eval pod, artifact publish
Post-Atlantis hookStart workflow from apply comment / webhook with change ticket search attrMaterialize Jobs that Atlantis should not run inline

Concrete commands & config

Temporal CLI — namespaces, workflows, visibility

# namespaces (blast radius)
temporal operator namespace list
temporal operator namespace describe --namespace fleet-ops
 
# start a durable cell-upgrade workflow
temporal workflow start \
  --namespace fleet-ops \
  --task-queue fleet-ops \
  --type CellUpgradeWorkflow \
  --workflow-id "upgrade-cell-a-2026-09-24" \
  --input '{"cell":"us-east-1-a","wave_size":5,"change_ticket":"ATL-1842"}'
 
# signal canary gate (inference promote)
temporal workflow signal \
  --namespace fleet-ops \
  --workflow-id "upgrade-cell-a-2026-09-24" \
  --name CanaryDecision \
  --input '{"promote":true,"digest":"sha256:abc…"}'
 
# describe + show history size pressure
temporal workflow describe --namespace fleet-ops --workflow-id "upgrade-cell-a-2026-09-24"
temporal workflow show --namespace fleet-ops --workflow-id "upgrade-cell-a-2026-09-24"
 
# visibility: find drains for a cell
temporal workflow list --namespace fleet-ops \
  --query "cell_id = 'us-east-1-a' AND ExecutionStatus = 'Running'"

Temporal worker sketch (determinism boundary)

// Workflow: deterministic orchestration only
func CellUpgradeWorkflow(ctx workflow.Context, in CellUpgradeInput) error {
    ao := workflow.ActivityOptions{
        StartToCloseTimeout: 30 * time.Minute,
        HeartbeatTimeout:    2 * time.Minute,
        RetryPolicy:         &temporal.RetryPolicy{MaximumAttempts: 5},
    }
    ctx = workflow.WithActivityOptions(ctx, ao)
 
    var drained int
    for wave := 0; wave*in.WaveSize < in.NodeCount; wave++ {
        if err := workflow.ExecuteActivity(ctx, DrainWaveActivity, in, wave).Get(ctx, &drained); err != nil {
            return err // or run compensation activity
        }
        // wait for human/metrics signal before next wave
        var decision CanaryDecision
        sigCtx, _ := workflow.WithCancel(ctx)
        workflow.GetSignalChannel(sigCtx, "CanaryDecision").Receive(sigCtx, &decision)
        if !decision.Promote {
            return workflow.ExecuteActivity(ctx, RollbackWaveActivity, in, wave).Get(ctx, nil)
        }
        // long-running controller: Continue-As-New periodically
        if workflow.GetInfo(ctx).GetContinueAsNewSuggested() {
            return workflow.NewContinueAsNewError(ctx, CellUpgradeWorkflow, in.AfterWave(wave))
        }
    }
    return nil
}
 
// Activity: real I/O, heartbeats, retries — NOT in workflow code
func DrainWaveActivity(ctx context.Context, in CellUpgradeInput, wave int) (int, error) {
    // kubectl / cloud API / internal fleet API — heartbeat while waiting
    return drainWave(ctx, in, wave)
}

Temporal mTLS / client config (sketch)

# workers and clients share the trust domain — no plaintext frontend in prod
temporal:
  hostPort: temporal-frontend.temporal.svc:7233
  namespace: fleet-ops
  tls:
    clientCertPath: /var/run/certs/client.pem
    clientKeyPath: /var/run/certs/client.key
    caPath: /var/run/certs/ca.pem
    serverName: temporal-frontend.temporal.svc
  # payload codec / encryption keys via env from Secret (not plaintext in ConfigMap)

Argo Workflow — ML checkpoint → validate → promote

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: ml-promote-
  namespace: ml-pipelines
  labels:
    pipeline: weights-promote
    cell: us-east-1-a
spec:
  serviceAccountName: argo-ml-pipeline   # least privilege — not default
  entrypoint: train-validate-promote
  activeDeadlineSeconds: 14400          # hard cap — no infinite GPU burn
  parallelism: 4
  podGC:
    strategy: OnWorkflowSuccess
    deleteDelayDuration: 1h
  artifactRepositoryRef:
    configMap: artifact-repositories
    key: default
  templates:
    - name: train-validate-promote
      dag:
        tasks:
          - name: train
            template: gpu-train
          - name: validate
            depends: train.Succeeded
            template: eval-metrics
          - name: promote
            depends: validate.Succeeded
            template: promote-serving
      # on any failure: notify Temporal / page — do not leave half-promoted weights
      onExit: exit-handler
 
    - name: gpu-train
      retryStrategy:
        limit: "2"
        retryPolicy: Always
        backoff:
          duration: "2m"
          factor: 2
      nodeSelector:
        nvidia.com/gpu.product: NVIDIA-H100
      container:
        image: registry.example.com/ml/train@sha256:…   # digest-pinned, signed (L12)
        imagePullSecrets: [{name: regcred}]
        resources:
          limits:
            nvidia.com/gpu: 8
        envFrom:
          - secretRef: {name: weights-reader}           # scoped GetObject only (L13)
        command: ["python", "-m", "train"]
      outputs:
        artifacts:
          - name: checkpoint
            path: /out/checkpoint
            s3:
              key: "checkpoints/{{workflow.name}}/ckpt"
 
    - name: eval-metrics
      container:
        image: registry.example.com/ml/eval@sha256:…
        command: ["python", "-m", "eval", "--fail-under", "0.92"]
      inputs:
        artifacts:
          - name: checkpoint
            path: /in/checkpoint
 
    - name: promote-serving
      resource:
        action: apply
        manifest: |
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: inference-canary
          spec:
            template:
              spec:
                containers:
                  - name: serve
                    image: registry.example.com/ml/serve@sha256:…
      # Prefer signaling Temporal to flip serving weight vs mutating prod directly from Argo
 
    - name: exit-handler
      steps:
        - - name: notify
            template: notify-temporal
            when: "{{workflow.status}} != Succeeded"

Argo CronWorkflow + RBAC scraps

apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
  name: nightly-cell-drain-canary
  namespace: fleet-ops
spec:
  schedule: "0 3 * * *"
  concurrencyPolicy: Forbid          # never overlap drain waves
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  workflowSpec:
    serviceAccountName: argo-drain
    activeDeadlineSeconds: 7200
    entrypoint: drain-canary
    templates:
      - name: drain-canary
        steps:
          - - name: drain
              templateRef:
                name: drain-wave-template
                template: drain
# --- RBAC: Workflow SA may create pods in its NS; may NOT patch cluster-scoped RBAC ---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: argo-ml-pipeline
  namespace: ml-pipelines
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: argo-ml-pipeline
  namespace: ml-pipelines
rules:
  - apiGroups: [""]
    resources: [pods, pods/log, configmaps, secrets]
    verbs: [get, list, watch, create, patch, delete]
  - apiGroups: ["argoproj.io"]
    resources: [workflows, workflowtaskresults]
    verbs: [get, list, watch, patch]
# Bind Role — never ClusterRole/cluster-admin for pipeline SAs

Submit / debug Argo

argo submit -n ml-pipelines --watch ml-promote.yaml
argo list -n ml-pipelines
argo get -n ml-pipelines ml-promote-xxxxx
argo logs -n ml-pipelines ml-promote-xxxxx
argo stop  -n ml-pipelines ml-promote-xxxxx    # graceful
argo terminate -n ml-pipelines ml-promote-xxxxx
 
# Pending forever? classic causes
kubectl -n ml-pipelines describe pod | grep -A20 "Events:"
kubectl -n ml-pipelines get resourcequota,limitrange
kubectl describe nodes | grep -A5 "nvidia.com/gpu"

Failures & fixes

SymptomLikely root causeFix
Temporal Nondeterminism after deployWorkflow code changed; replay divergesGetVersion / worker versioning; never silently edit branching of open WFs; pin worker builds
Workflow latency spikes / history limitUnbounded loop; chatty activities; missing Continue-As-NewContinue-As-New on suggest; shrink payloads; move bulk data to object store + pass refs
Activities timing out / worker “idle” but backlog growsNot enough pollers; wrong task queue; sticky thrash; no heartbeat on long activityScale workers; fix queue names; heartbeat; raise HeartbeatTimeout appropriately
Schedule fires twice / missedMultiple Schedule writers; clock skew assumptions; namespace confusionSingle owner per Schedule ID; use Temporal Schedule not homemade Cron+Start
Argo Workflow stuck PendingPVC unbound; ResourceQuota; GPU shortage; imagePullBackoff; affinityDescribe pod/events; fix storage class; request GPU nodes; digest-pinned pulls with pull secret
Artifact step fails with AccessDeniedBucket IAM / wrong key prefix / missing KMS decryptScoped IAM for WF SA / IRSA; CMEK grant; never make bucket public to “unblock”
Retry storm burns GPU quotaretryStrategy without activeDeadlineSeconds / Forbid concurrencyHard deadlines; concurrencyPolicy: Forbid; semaphore on GPU templates
Zombie Workflow Running foreverMissing deadline; exit handler never fires; controller overloadactiveDeadlineSeconds; controller HA + rate limits; terminate + alert on age
Half-promoted model in servingPromote step ran after failed validate; no outer gateDAG depends: validate.Succeeded; Temporal signal gate; atomic serving pointer update
Secret in Temporal payload plaintextActivity args logged/visibilityPayload encryption/codec; pass secret refs/handles not values; scrub visibility

Security & hardening

This is the execution plane sitting on everything L11–L15 hardened. A perfect PSS profile still loses if an Argo SA is cluster-admin, Temporal payloads carry weight URLs with standing keys, or artifact buckets are world-readable.

Temporal controls

ControlOperator practice
mTLS everywhereClients, workers, frontend — private CA; rotate; no plaintext 7233 on a shared VPC “because internal”
Authn / authz per namespaceJIT access for break-glass; operators ≠ application writers
Encrypt payloadsCodec / encryption so history store & visibility do not become a secret lake
Least-priv activity IAMActivity workers assume scoped cloud roles (cell drain API, weight reader, never s3:*)
NetworkPolicyWorkers egress only to frontend + required APIs; frontend not world-reachable
AuditWorkflow ID = change ticket; search attributes carry change_ticket, actor

Argo controls

ControlOperator practice
Workflow SA least privilegeRole in namespace only; no cluster-admin; separate SA for ML vs fleet drain
Deny privileged / hostPathPSS restricted + admission (L11 / L14); workflows invent pods — treat them as untrusted compute
Signed, digest-pinned imagesCosign verify / admission; no :latest in templates (L12)
Private registriesimagePullSecrets or node identity; block Docker Hub surprise pulls in prod NS
Artifact bucket IAMPrivate bucket; CMEK; deny public ACLs; prefix-scoped read/write per pipeline
SecretsK8s Secrets / external secrets → env; never bake long-lived cloud keys into templates; prefer WI/IRSA
NetworkPolicyPipeline pods talk to registry, artifact bucket endpoint, model store — not the flat cluster network
Controller hardeningArgo server SSO/RBAC; disable --auth-mode=server anonymous; audit who can argo submit

AI/ML angle (required)

Workflows that pull model weights/checkpoints, train, or promote serving inherit L12–L14:

RiskHardening
Weight exfil via artifact repoPrivate bucket; no allUsers; separate writer (train) vs reader (serve); CMEK (L13)
Unsigned train/serve imageDigest pin + cosign verify in admission before Workflow pods schedule (L12)
Standing weight credentials in WF specShort-lived WI/IRSA on the step SA; Secret store refs; Temporal passes handles, not keys
GPU pod escape / lateral movePSS restricted; no privileged; NetPol; GPU pool isolation (L14)
Provenance gapRecord image digest + checkpoint hash + Workflow UID in Temporal search attrs / artifact metadata (SLSA mindset)
World-readable eval artifactsSame bucket controls as weights; TTL delete; no anonymous HTTPS listing
# Sketch NetworkPolicy for ml-pipelines Workflow pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ml-pipeline-egress
  namespace: ml-pipelines
spec:
  podSelector:
    matchLabels:
      workflows.argoproj.io/workflow: ""  # tighten with your labels
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels: {kubernetes.io/metadata.name: kube-system}
      # DNS — prefer explicit CoreDNS selectors in real clusters
    - to:
        - podSelector: {}  # replace with registry / artifact gateway selectors
      ports:
        - {protocol: TCP, port: 443}

Punchline: Temporal durability without mTLS/encrypted payloads is a durable secret leak. Argo convenience without SA/image/bucket discipline is a GPU-powered exfil path. L15 decides who may change the substrate; L16 decides who may run code that touches weights and cells.

Interview soundbites

  • “Temporal for durable multi-day fleet sagas with signals; Argo for K8s-native GPU/batch DAGs — compose, don’t collapse.”
  • “Workflow code is deterministic; activities do I/O — nondeterminism after deploy is a versioning bug, not bad luck.”
  • “History bloat means you needed Continue-As-New yesterday.”
  • “Task queues are blast-radius and capacity boundaries — starve or storm starts with queue design.”
  • “CronWorkflow without Forbid concurrency and activeDeadlineSeconds is how you DDoS your own GPU pool.”
  • “Artifacts are data planes: private buckets, scoped IAM, CMEK — never world-readable checkpoints.”
  • “Workflow SAs are least privilege; PSS restricted; digest-pinned signed images only.”
  • “Promote is a Temporal signal gated on validate — not an Argo step that always runs.”
  • “Atlantis applies; Temporal orchestrates; Argo executes pods — three planes, one change ticket.”
  • “mTLS to Temporal frontend; payload encryption; NetPol on workers — durable ≠ trusted.”
  • “Search attributes carry cell_id and change_ticket — oncall finds the wave without SSH folklore.”
  • “Zombie Running workflows get deadlines, GC, and alerts — hope is not an SRE strategy.”

How this maps to the JD

Hyperscale platforms do not “SSH and drain.” They run durable orchestration across 100+ clusters: upgrades, drains, canaries, and ML promote paths that survive process death and human sleep. Temporal maps to the Borg-like control instincts from L4 with modern durable execution; Argo maps to K8s-native batch/DAG execution on the cells L3 provisioned and L15 changed safely. Security posture composes L11–L14: RBAC, signed images, weight IAM, admission — now applied to workflow runners themselves. Next curriculum beat: systems design tradeoffs for rapidly evolving platforms, then the deeper AI/ML GPU track.

Watch (talks & demos)

Real talks that map to this lesson — watch after the Failures & fixes table.

Declarative and Imperative Kubernetes Operations with Temporal and M3 (KubeCon NA 2021)

Chronosphere + Temporal: why operators detect drift declaratively while Temporal runs imperative mitigation (upgrades, quorum-safe rolls) — the exact compose model for fleet ops.

Open on YouTube (opens in a new tab)

Large Scale Batch Processing with Argo Workflows and Events (Intuit / KubeCon)

~40k pipelines, multi-cluster Argo, HA controller, PDBs, rate limits, semaphores — operator depth for Argo at fleet/batch scale (including ML-adjacent data pipelines).

Open on YouTube (opens in a new tab)

Getting to know Temporal (Maxim Fateev)

Founder-level walkthrough of durable execution, workflow vs activity, retries, and why this replaces homemade saga/queue folklore — grounding for the Temporal half of the lesson.

Open on YouTube (opens in a new tab)

References

Quick self-check

Out loud, in under two minutes:

  1. When do you choose Temporal over Argo Workflows — and when do you compose both?
  2. Why must Temporal workflow code be deterministic, and what breaks if you change it under open executions?
  3. What does Continue-As-New buy you operationally at fleet scale?
  4. Name three reasons an Argo Workflow stays Pending and how you diagnose each.
  5. How do you harden an ML promote pipeline (images, weight IAM, SA, artifacts, promote gate)?
  6. How does L16 sit on top of L15 (Atlantis) and L12–L14 (signing / weights / deploy hardening)?