Tools · Argo Workflows

Argo Workflows

Argo Workflows is a Kubernetes-native workflow engine: you describe a multi-step job as a custom resource, and the controller runs every step as a pod, passing parameters and files between them, retrying what fails, and cleaning up after itself. It solves the platform problem of “I need to run a sequence of containers with dependencies, on the cluster I already have, without buying a separate orchestration product” — CI builds, batch data pipelines, nightly maintenance, ML training, cluster automation. If Tekton is a CI/CD framework, Argo Workflows is a general-purpose orchestrator that happens to be excellent at CI.

☺ Explain it like I’m 10

Think of a recipe card for baking a cake. Step 1: mix the batter. Step 2: bake it. Step 3: make the icing — but you can do that while the cake bakes. Step 4: only once the cake is cool and the icing is ready, decorate it. Argo Workflows is a robot chef that reads that card. Each step gets its own little kitchen (a pod), the robot knows which steps can happen at the same time and which have to wait, it hands the batter from one kitchen to the next, and if a step burns it tries again. You write the recipe card as a YAML file; the robot does the rest.

🦫Your host for this topic: Benny the Beaver — Benny builds the rails that work travels on, and a workflow engine is the most literal set of rails on the whole platform.

What it is and the problem it solves

☺ Like you’re 10: Kubernetes is great at running one container. It’s bad at running six containers in a particular order. Argo Workflows fills that hole.

Kubernetes gives you a Pod for “run this container” and a Job for “run this container until it succeeds.” Neither gives you “run these six containers, some in parallel, some only after others finish, passing a build artefact from step two into step five, and retry step four if the network flakes.” Teams that need that reach for a CronJob wrapped around a giant shell script, a Jenkins server bolted on the side, or a home-grown Python driver — and every one of those puts orchestration logic outside the cluster’s declarative API, where it can’t be reviewed, versioned, or reconciled.

Everything is a pod, every pipeline is an API object

Argo Workflows takes the opposite bet. A workflow is a Kubernetes custom resource (Workflow, in the argoproj.io/v1alpha1 group). Each step becomes a real pod, scheduled by the real scheduler, subject to your real quotas, network policies, and node affinity — no agent fleet, no build servers to patch. The controller watches Workflow objects, creates pods in dependency order, and records each step’s status back into the object.

Because a workflow is just an API object, everything you already know applies: kubectl get workflows lists them, RBAC governs who can create them, admission policy from Kyverno or Gatekeeper can validate them, and GitOps can deliver them.

Not a CI tool — an orchestration tool

The line to remember: Tekton is CI/CD pipelines; Argo Workflows is general orchestration of Kubernetes resources. Tekton’s vocabulary is built around shipping software — Task, Pipeline, PipelineRun, workspaces, a catalogue of build steps. Argo Workflows’ vocabulary is deliberately generic — templates, DAGs, parameters, artifacts — so the same engine runs a CI build, a genomics pipeline, a nightly database compaction, an ML training sweep, or cluster automation that creates and waits on other Kubernetes objects.

◆ Key idea

Argo Workflows is compute orchestration expressed as a Kubernetes API. Its unit of work is a pod; its unit of composition is a template; its unit of delivery is a CRD you can commit to Git. If your problem is “several containers, in some order, sharing data,” it fits. If your problem is “one long-running service,” it does not — that’s a Deployment.

Where it fits in a platform

☺ Like you’re 10: It lives in the “getting work done” part of the platform, not the “keeping websites up” part.

On the platform architecture map, Argo Workflows sits in the delivery and automation plane — the machinery that turns an event or a schedule into work on the Kubernetes substrate. It is not the control plane that keeps services healthy, and it is not a portal. It is the engine behind “something has to happen now, in several stages.”

How it relates to its neighbours

The exam’s tool list simply names “Argo,” so you need the family map straight. Argo CD reconciles desired state from Git — a continuous loop with no end. Argo Rollouts replaces Deployment to give you canary and blue/green promotion. Argo Workflows runs finite jobs that start, do something, and end. Argo Events (the fourth sibling, not on the exam list) turns webhooks and queue messages into workflow submissions.

ProjectShape of workTypical triggerEnds?
Argo CDReconcile cluster to GitGit commit / poll timerNever — it’s a loop
Argo RolloutsShift traffic between versionsNew image in a RolloutEnds at promotion
Argo WorkflowsRun a DAG of podsargo submit, cron, eventYes — Succeeded or Failed
TektonRun a CI/CD pipeline of podsTrigger / PipelineRunYes

In a full reference architecture, the pattern is: Argo Workflows (or Tekton) builds and signs the image and writes a new tag into the config repo; Argo CD notices the commit and syncs; Argo Rollouts performs the safe rollout. Three tools, three jobs, one path from commit to production — told end-to-end in CI/CD & Progressive Delivery.

CNPE domain relevance

Workflows show up in more than one domain. Most obviously D2 · GitOps & Continuous Delivery, since “Argo” is on the official tool list. But it also lands in Platform Engineering Core whenever it is the execution engine behind a self-service action (“give me a seeded test database”), and in Observability, because workflow pods emit logs and metrics like everything else. Treat it as the platform’s general-purpose “do a thing” button.

🦆 Dot’s-eye view

“I don’t write workflow YAML. My team’s platform gives me a button in Backstage called Refresh my staging data. Under the hood it submits a workflow from a ClusterWorkflowTemplate with my namespace as a parameter. I see a progress bar and a log tail. That’s the whole interface, and honestly that’s all I want.”

How it works — architecture and CRDs

☺ Like you’re 10: One controller reads your recipe cards and makes pods. A second, optional piece is the website you watch them on.

The runtime is small. The workflow-controller is a single deployment that watches Workflow objects, walks the dependency graph, creates one pod per step, and writes status back. Each workflow pod runs your container alongside an executor — the emissary executor, which is the default in modern releases — that wraps your command, captures its outputs, and copies artifacts in and out. In older versions you had to choose an executor; today you do not, so treat it as an implementation detail rather than a knob. The argo-server is an optional API and web UI; the CLI talks either directly to the Kubernetes API or to argo-server. Artifacts live in an external object store — S3, MinIO, GCS, Azure Blob — because pods are ephemeral and a file written in step one has to survive until step five.

🦫 argo submit or cron / event Workflow CR in the Kubernetes API workflow-controller walks the DAG argo-server UI + API (optional) pod: build pod: test pod: scan pod: push S3 / MinIO artifact repo watch creates one pod per step, in dependency order artifacts build → (test ∥ scan) → push · parallel where the DAG allows, serial where it must no build agents, no separate scheduler — just pods and one controller

The four custom resources you author

Argo Workflows installs a fair number of CRDs, but only four are ones a human writes. Know these four by name and by purpose — this is the most likely thing to be asked. All four live in the argoproj.io/v1alpha1 API group.

KindScopekubectl short nameWhat it is for
WorkflowNamespacedwfOne execution, holding both the definition and the live status. It lives on after completion so you can read the result.
WorkflowTemplateNamespacedwftmplA reusable definition that does not run by itself. Submit from it, or reference it with templateRef / workflowTemplateRef.
ClusterWorkflowTemplateClustercwftmplThe same, cluster-scoped, so a platform team can publish golden pipelines every namespace can call.
CronWorkflowNamespacedcwfA schedule wrapped around a workflow spec. It is to Workflow what CronJob is to Job.

The rest are machinery the controller and executor use among themselves — WorkflowTaskResult is the one you will meet, because every step pod writes one and the RBAC to do so is the single most common installation mistake (see the gotchas below). Argo also ships WorkflowEventBinding for turning API events into submissions. You do not hand-write any of these; you only grant permission on them.

Template types — the real vocabulary

☺ Like you’re 10: A “template” is one instruction on the recipe card. Two kinds of instruction are just “here’s a list of other instructions.”

spec.templates is a list of named templates; spec.entrypoint says which one starts. They come in two families: leaf templates that do something, and orchestration templates that call other templates.

TemplateFamilyWhat it does
containerLeafA single container — the same schema as a pod container spec.
scriptLeafA container plus an inline source body (bash, python, node). Its stdout is captured as outputs.result.
resourceLeafcreate, apply, delete, patch or get on an arbitrary Kubernetes object; can block until a successCondition is met. This is what makes Argo an orchestrator of Kubernetes resources.
dagOrchestrationtasks, each naming a template and its dependencies. Anything whose dependencies are satisfied runs at once.
stepsOrchestrationA list of lists: outer = sequential stages, inner = parallel within a stage.
suspendOrchestrationPauses for a duration or until argo resume — your manual approval gate.

Those six carry almost every real pipeline. A handful of others exist for narrower jobs — containerSet runs several containers inside one pod so they can share a volume without a PVC, and http makes a request without spending a pod at all. Learn the six first; reach for the rest only when the six are genuinely awkward.

Fan-out is a property of the calling task, not a template type: withItems expands a task over a hard-coded list, withParam expands it over a JSON array produced by an earlier step, and withSequence expands it over a numeric range. Each expansion produces its own pod, which is exactly why the parallelism caps in the gotchas section matter.

◆ Key idea

dag and steps express the same thing and you can nest one inside the other. Use steps when the pipeline is basically a line; use dag when the shape is a genuine graph and you want the engine to squeeze out every bit of parallelism. Both call the same leaf templates, so the choice is presentational, not architectural.

The resources you will actually write

☺ Like you’re 10: Here are real recipe cards — copy the shapes, not the words.

A DAG workflow with parameters, artifacts and retries

The canonical build pipeline: clone, then test and scan in parallel, then push. Note the parameter plumbing ({{workflow.parameters.*}}, {{inputs.parameters.*}}), the artifact handoff, and the housekeeping fields near the top that stop your cluster filling with dead pods.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: build-           # the API server appends a random suffix per run
  namespace: ci
spec:
  entrypoint: pipeline
  serviceAccountName: workflow-runner    # NOT "default" — see the RBAC gotcha
  arguments:
    parameters:
      - name: repo
        value: https://github.com/acme/checkout.git
      - name: revision
        value: main
  ttlStrategy:
    secondsAfterCompletion: 86400        # delete the Workflow object after 1 day
  podGC:
    strategy: OnWorkflowSuccess          # delete step pods once the run succeeds
  templates:
    - name: pipeline
      dag:
        tasks:
          - name: clone
            template: clone
            arguments:
              parameters:
                - { name: repo, value: "{{workflow.parameters.repo}}" }
                - { name: revision, value: "{{workflow.parameters.revision}}" }
          - name: test
            template: run-make
            dependencies: [clone]        # test and scan both wait on clone…
            arguments:
              parameters: [{ name: target, value: test }]
              artifacts:
                - name: src
                  from: "{{tasks.clone.outputs.artifacts.src}}"
          - name: scan
            template: run-make
            dependencies: [clone]        # …and then run in parallel with each other
            arguments:
              parameters: [{ name: target, value: scan }]
              artifacts:
                - name: src
                  from: "{{tasks.clone.outputs.artifacts.src}}"
          - name: push
            template: run-make
            dependencies: [test, scan]   # push waits for BOTH
            arguments:
              parameters: [{ name: target, value: push }]
              artifacts:
                - name: src
                  from: "{{tasks.clone.outputs.artifacts.src}}"

    - name: clone
      inputs:
        parameters: [{ name: repo }, { name: revision }]
      container:
        image: alpine/git        # pin to a digest in production
        command: [sh, -c]
        args: ["git clone --depth 1 -b {{inputs.parameters.revision}} {{inputs.parameters.repo}} /work/src"]
      outputs:
        artifacts:
          - name: src
            path: /work/src              # tarred and uploaded to the artifact repo
      retryStrategy:
        limit: "3"
        retryPolicy: OnTransientError    # network flakes only, not real failures
        backoff: { duration: "10s", factor: "2", maxDuration: "2m" }

    - name: run-make
      inputs:
        parameters: [{ name: target }]
        artifacts:
          - name: src
            path: /work/src              # downloaded from the artifact repo
      container:
        image: ghcr.io/acme/builder:1.9
        workingDir: /work/src
        command: [make]
        args: ["{{inputs.parameters.target}}"]
        resources:
          requests: { cpu: "500m", memory: 1Gi }
          limits:   { memory: 2Gi }

A reusable WorkflowTemplate driven by a CronWorkflow

Real platforms almost never submit raw Workflow objects. The platform team publishes a WorkflowTemplate (or ClusterWorkflowTemplate), and everything else — a cron schedule, a portal button, an event — references it by name. Below, a nightly report uses a steps template with a script step whose stdout feeds the next step, and a CronWorkflow that fires it. Note the schedule field: recent versions take a list under schedules, while the single schedule string you will still see in older examples is deprecated. Both are accepted today; write the list. If a cluster rejects the list form, it is running an older controller — that alone tells you something useful about the environment.

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: nightly-report
  namespace: data
spec:
  entrypoint: main
  serviceAccountName: workflow-runner
  templates:
    - name: main
      steps:                              # outer list = sequential stages
        - - name: pick-partition           # inner list = parallel within a stage
            template: pick
        - - name: crunch-eu
            template: crunch
            arguments:
              parameters:
                - { name: region, value: eu }
                - { name: partition, value: "{{steps.pick-partition.outputs.result}}" }
          - name: crunch-us                # crunch-eu and crunch-us run together
            template: crunch
            arguments:
              parameters:
                - { name: region, value: us }
                - { name: partition, value: "{{steps.pick-partition.outputs.result}}" }
        - - name: approve
            template: hold                 # manual gate before publishing
        - - name: publish
            template: crunch
            arguments:
              parameters:
                - { name: region, value: all }
                - { name: partition, value: "{{steps.pick-partition.outputs.result}}" }

    - name: pick
      script:                              # stdout becomes outputs.result
        image: python:3.12-alpine
        command: [python]
        source: |
          import datetime
          print((datetime.date.today() - datetime.timedelta(days=1)).isoformat())

    - name: hold
      suspend: {}                          # waits forever until `argo resume`

    - name: crunch
      inputs:
        parameters: [{ name: region }, { name: partition }]
      container:
        image: ghcr.io/acme/reporter:2.3
        args: ["--region={{inputs.parameters.region}}", "--date={{inputs.parameters.partition}}"]
---
apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
  name: nightly-report
  namespace: data
spec:
  schedules:                             # a LIST since v3.6; older docs show a single `schedule:` string
    - "17 2 * * *"
  timezone: Europe/London
  concurrencyPolicy: Forbid              # skip if the previous run is still going
  startingDeadlineSeconds: 300           # if the controller was down, don't fire late
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5
  workflowSpec:
    workflowTemplateRef:
      name: nightly-report               # add clusterScope: true for a ClusterWorkflowTemplate
⚠ suspend is not free

A suspend template holds the whole Workflow object open indefinitely — no pods are running, but the object sits in Running and ttlStrategy never fires. Combined with concurrencyPolicy: Forbid, one forgotten approval silently stops every subsequent nightly run. Always pair an open-ended approval gate with an alert, or use suspend: { duration: "24h" } so it eventually times out.

The resource template — and the RBAC it demands

This is the template that makes Argo Workflows a Kubernetes orchestrator. It creates an arbitrary object, then blocks until a successCondition on that object becomes true. The condition syntax is deliberately simple — a dotted field path compared with ==, or tested with in (…) against a set — evaluated by re-reading the object, so it cannot express arbitrary JSONPath filters over lists. Here a workflow provisions an ephemeral test database through a custom provisioning CRD, waits for it to be ready, and gets the RBAC it needs. A real Crossplane claim reports readiness through its status.conditions array rather than a single status.phase field, which is exactly the kind of shape the simple condition syntax struggles with — check the CRD you are actually waiting on before you write the condition.

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata: { name: ephemeral-db, namespace: ci }
spec:
  entrypoint: provision
  serviceAccountName: workflow-runner
  templates:
    - name: provision
      resource:
        action: create
        setOwnerReference: true            # object is GC'd when the Workflow is deleted
        successCondition: status.phase == Ready
        failureCondition: status.phase == Failed
        manifest: |
          apiVersion: acme.io/v1alpha1
          kind: PostgresClaim
          metadata:
            generateName: test-db-
          spec:
            size: small
            version: "16"
      outputs:
        parameters:
          - name: dbname
            valueFrom:
              jsonPath: '{.metadata.name}' # feed the created name to later steps
---
# The ServiceAccount the workflow runs as, and the permissions it needs.
apiVersion: v1
kind: ServiceAccount
metadata: { name: workflow-runner, namespace: ci }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: workflow-runner, namespace: ci }
rules:
  # Required by the executor for EVERY workflow — this is the one people forget:
  - apiGroups: ["argoproj.io"]
    resources: ["workflowtaskresults"]
    verbs: ["create", "patch"]
  # Required only because we use a resource template against PostgresClaim:
  - apiGroups: ["acme.io"]
    resources: ["postgresclaims"]
    verbs: ["get", "list", "watch", "create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: workflow-runner, namespace: ci }
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: Role, name: workflow-runner }
subjects:
  - { kind: ServiceAccount, name: workflow-runner, namespace: ci }

Artifact repository and shared volumes

Artifacts need somewhere to live. The controller reads a default from its own ConfigMap, but the cleanest per-namespace pattern is an artifact-repositories ConfigMap holding one named repository per key. An annotation on that ConfigMap nominates the key to use when nothing else is said; a workflow that wants a different key names both the ConfigMap and the key in spec.artifactRepositoryRef. If your steps only need scratch space within one run and never leave the cluster, a volumeClaimTemplate is simpler and much faster than round-tripping through S3.

apiVersion: v1
kind: ConfigMap
metadata:
  name: artifact-repositories
  namespace: ci
  annotations:
    workflows.argoproj.io/default-artifact-repository: minio-store
data:
  minio-store: |
    s3:
      endpoint: minio.minio.svc:9000
      insecure: true                 # in-cluster MinIO over plain HTTP
      bucket: argo-artifacts
      keyFormat: "{{workflow.namespace}}/{{workflow.name}}/{{pod.name}}"
      accessKeySecret: { name: minio-creds, key: accesskey }
      secretKeySecret: { name: minio-creds, key: secretkey }
---
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata: { generateName: shared-scratch-, namespace: ci }
spec:
  entrypoint: main
  artifactRepositoryRef:             # optional: pick a key from the ConfigMap above
    configMap: artifact-repositories
    key: minio-store
  volumeClaimTemplates:              # one PVC created per run, deleted with the run
    - metadata: { name: scratch }
      spec:
        accessModes: [ ReadWriteOnce ]
        storageClassName: fast-ssd
        resources: { requests: { storage: 20Gi } }
  templates:
    - name: main
      steps:
        - - { name: generate, template: writer }
        - - { name: consume,  template: reader }
    - name: writer
      container:
        image: busybox:1.36
        command: [sh, -c, "dd if=/dev/urandom of=/scratch/blob bs=1M count=64"]
        volumeMounts: [{ name: scratch, mountPath: /scratch }]
    - name: reader
      container:
        image: busybox:1.36
        command: [sh, -c, "wc -c /scratch/blob"]
        volumeMounts: [{ name: scratch, mountPath: /scratch }]

☺ Like you’re 10: Artifacts are “post it to a locker in the cloud so any later step can fetch it.” A shared volume is “everyone works on the same desk.” The desk is faster; the locker works across nodes and outlives the run.

Day-to-day commands

☺ Like you’re 10: There’s a little program called argo that talks to the robot chef. Six commands cover almost everything.

The argo CLI

# Submit and follow. --watch prints a live tree; -p sets a parameter.
argo submit -n ci build.yaml --watch -p revision=release-2.1

# Submit FROM a stored template (the way real platforms do it)
argo submit -n data --from workflowtemplate/nightly-report --watch
argo submit -n ci   --from clusterworkflowtemplate/golden-build -p repo=https://...

# See what's happening
argo list -n ci                        # all workflows, newest first
argo list -n ci --status Running
argo get  -n ci @latest                # @latest = most recently submitted here
argo watch -n ci build-9k2xp           # live-updating tree view
argo logs -n ci build-9k2xp -f         # stream all step logs
argo logs -n ci build-9k2xp -c main    # one container of one step

# Fix things
argo retry     -n ci build-9k2xp       # re-run ONLY the failed nodes, keep successes
argo resubmit  -n ci build-9k2xp       # brand-new run from the same spec
argo stop      -n ci build-9k2xp       # graceful: exit handlers still run
argo terminate -n ci build-9k2xp       # immediate: kill everything now
argo resume    -n data nightly-report-xxxxx   # release a suspend/approval gate
argo delete    -n ci --completed --older 7d

# Templates and schedules
argo template create wt.yaml -n data
argo template list   -n data
argo cron list       -n data
argo cron suspend nightly-report -n data
argo lint build.yaml                   # validate BEFORE you submit
⚠ stop vs terminate

These are not synonyms and the difference is exam-worthy. argo stop shuts the workflow down gracefully — running steps are told to finish and exit handlers still run, so your cleanup and notification steps happen. argo terminate kills the workflow immediately and skips exit handlers. If your workflow provisions cloud resources in a resource template, terminating it can leave them orphaned.

When the CLI isn’t there

Everything is a CRD, so plain kubectl works and is the fallback you want in an exam terminal or a locked-down cluster. Knowing this pair of equivalences is worth real marks.

kubectl get workflows -n ci                     # short name: wf
kubectl get wf -n ci -o wide
kubectl describe wf build-9k2xp -n ci
kubectl get wf build-9k2xp -n ci -o jsonpath='{.status.phase}'   # Pending|Running|Succeeded|Failed|Error
kubectl create -f build.yaml -n ci              # `create`, not `apply` — generateName
kubectl delete wf --all -n ci

# The controller labels workflows and their pods — useful when the CLI is missing
kubectl get wf -n ci -l workflows.argoproj.io/phase=Failed
kubectl get wf -n ci -l workflows.argoproj.io/completed=true
kubectl get pods -n ci -l workflows.argoproj.io/workflow=build-9k2xp
kubectl logs -n ci build-9k2xp-clone-1234567890 -c main          # `main` = your container

# The controller's own health and config
kubectl -n argo logs deploy/workflow-controller -f
kubectl -n argo get cm workflow-controller-configmap -o yaml
kubectl get workflowtemplates,clusterworkflowtemplates,cronworkflows -A

Note kubectl create rather than apply for a workflow that uses generateNameapply needs a fixed metadata.name and will reject the object. See the command reference for the wider kubectl muscle memory.

Gotchas and failure modes

☺ Like you’re 10: Three things trip up almost everybody: permissions, where files live, and rubbish not getting taken out.

RBAC — the number-one support ticket

Workflow pods run as a ServiceAccount, and by default that is the namespace’s default SA, which has no permissions at all. Two failures follow. First, the executor needs create and patch on workflowtaskresults in argoproj.io to report step outputs — without it, steps run fine but the workflow fails at the end with workflowtaskresults is forbidden. Second, a resource template acts on the cluster as that ServiceAccount, so it needs explicit verbs on the target kind, including get/list/watch if you use a successCondition (Argo has to keep reading the object to evaluate it). Always create a dedicated SA and set serviceAccountName; if a step dies with a 403, that’s your answer. Two refinements worth knowing: serviceAccountName can be set per template as well as on the whole spec, so a single workflow can run its risky resource step under a more privileged identity while every other step stays minimal; and if you enable artifact garbage collection, the controller needs its own permission on the artifact GC task resource — a separate failure with a similar-looking error. General approach: Triage: delivery.

Artifact repository misconfiguration

Artifact failures are late and cryptic. If no repository is configured, workflows that don’t use artifacts run happily and workflows that do fail at upload time — so the problem appears only when someone adds an output. Watch for: the bucket doesn’t exist (Argo does not create it by default), the credentials Secret is in a different namespace from the workflow (it must be in the workflow’s namespace), insecure: true missing for plain-HTTP in-cluster MinIO, and a keyFormat that collides between runs. Whatever store you pick is a real dependency: if S3 is down, your pipelines are down. Background: Storage & State.

Pod and workflow cleanup

By default, completed workflow pods and completed Workflow objects stay. A busy CI namespace accumulates thousands of Completed pods, which bloats etcd, slows every kubectl get pods, and eventually trips node pod limits. Two independent knobs fix it and you need both: podGC.strategy (OnPodCompletion, OnPodSuccess, OnWorkflowCompletion, OnWorkflowSuccess) deletes the pods; ttlStrategy (secondsAfterCompletion, secondsAfterSuccess, secondsAfterFailure) deletes the Workflow object. Note that podGC also takes a label selector, so you can keep the pods of one labelled step for inspection while sweeping the rest. Set both as controller-wide defaults in the controller ConfigMap rather than trusting every author to remember them. But be deliberate: aggressive pod GC deletes the pods whose logs you wanted, so pair it with archiveLogs or log shipping into Loki.

⚠ Three more that bite

Resource limits: workflow steps are pods, so an unbounded fan-out (withItems over 500 entries) will happily try to schedule 500 pods — set spec.parallelism and a namespace ResourceQuota, and read Scaling & Scheduling. The outputs.result size cap: a script’s captured stdout is stored inside the Workflow object’s status, which lands in etcd — so it is capped, and a run that prints megabytes will be truncated or will fail the whole object. Pass anything larger than a short string as an artifact, not a result. Secrets: never interpolate a secret into a parameter; parameters end up in the Workflow object and the UI in plain text. Mount secrets as env vars or files, as Secrets Management describes.

Alternatives and when to choose it

☺ Like you’re 10: Other robot chefs exist. Pick by what you’re cooking.

The field of options

Nothing about Argo Workflows is exotic; every one of its jobs has a competitor that does it differently. What matters for the exam and for real design reviews is being able to say why you picked one, in terms of where the orchestration state lives and who can govern it.

OptionBest atWeak atChoose it when…
Argo WorkflowsGeneral DAG orchestration; batch, ML, data, cluster automation via the resource templateNo opinionated CI catalogue — you assemble build steps yourselfYou need one engine for many kinds of multi-step job
TektonCI/CD specifically — Tasks, Pipelines, a shared catalogue, triggers, supply-chain integrationLess natural for non-CI batch workYour problem is squarely “build, test, scan, sign, publish”
Kubernetes Job/CronJobOne container, on a schedule, zero extra componentsNo dependencies, no data passing, no UIThe work genuinely is a single container
Jenkins / GitLab CI / GitHub ActionsHuge plugin ecosystems, familiarity, hosted runnersAgents and state outside the cluster; pipelines aren’t API objects, so cluster policy can’t govern themYou already run one and the work isn’t Kubernetes-shaped
Airflow / DagsterData engineering: backfills, lineage, Python-native authoringHeavier to operate; its own scheduler and metadata DBData pipelines are your primary workload

The honest summary: choose Argo Workflows when your orchestration needs are varied and your platform is Kubernetes-first; Tekton when they are narrowly CI/CD; a plain CronJob when there is only one container — a surprisingly common right answer, and one the anti-patterns page will nag you about.

The test that actually decides it

Ask one question: should this pipeline be governed by the cluster? If the answer is yes — you want admission policy to inspect it, RBAC to constrain it, quotas to bound it, and GitOps to deliver it — then the pipeline must be an API object, and that rules out anything whose definition lives in a vendor’s database or a .yml file interpreted by an external runner. If the answer is no, an external CI system you already run is very often the cheaper choice, and adopting a second orchestrator to avoid it is the more expensive mistake.

The second question is about shape, not governance: how much of your work is a genuine graph? A single container is a CronJob. A straight line of build stages is comfortable in either Tekton or Argo Workflows. A real fan-out and fan-in — especially one whose width is computed at runtime — is where Argo Workflows starts earning its keep, because withParam over an earlier step’s output has no clean equivalent in most alternatives.

Running two engines on purpose

Plenty of mature platforms run Tekton for application CI and Argo Workflows for everything else, and that is a defensible choice rather than a failure to decide — the two have different audiences and different blast radii. What is not defensible is running both for the same job because two teams each picked one. If you carry two engines, write down the boundary, put it in the developer experience docs, and make the self-service layer hide the difference. The platform guild exists to settle exactly this kind of argument once instead of per team.

🦫 Benny’s workshop · 25 min

On a throwaway kind cluster, install Argo Workflows into the argo namespace and port-forward the UI. (1) Write a three-node dag where two steps depend on one — confirm in the UI that the two really do run at the same time. (2) Make one step exit 1, add retryStrategy: { limit: "2" }, and watch Argo retry it. (3) Add a resource template that creates a ConfigMap — it will fail with a 403 until you build the Role and RoleBinding yourself. That is the single most valuable twenty minutes you can spend on this tool. (4) Finally set podGC.strategy: OnWorkflowSuccess and ttlStrategy.secondsAfterCompletion: 60, submit again, and watch the pods and then the Workflow object disappear.

🎬 At the Platform Guild
🦊

Foxy: So Argo Workflows is just Argo CD for jobs, right? Same tool, different tab?

🦫

Benny: Different project entirely — they only share a name and a logo. Argo CD is a loop that never ends. Argo Workflows runs a graph of pods and then stops.

🐦

Pip: And neither of them is Argo Rollouts, which is mine. Three siblings, three jobs. The exam just says “Argo,” so know all three.

👺

Gizmo: I gave every workflow cluster-admin. The 403s stopped instantly! Efficiency! 🤑

🐢

Timmy: Gizmo. A workflow is a pod running someone’s arbitrary container. You just gave every developer’s YAML root on the cluster. One ServiceAccount per workflow, one Role with the exact verbs it needs.

🦫

Benny: And turn on pod GC before you leave. Last team I visited had forty thousand Completed pods and wondered why the API server was crying.

🦆

Dot: I just press the button in the portal. Please keep it that way.

Exam relevance and going further

☺ Like you’re 10: In the exam you can’t look this up on Argo’s website. Learn the shapes now.

What to be able to do cold

“Argo” is on the official CNPE tool list, and the exam is performance-based, so the bar is doing. Be able to: name the four CRDs and their purposes; explain and write both dag and steps; explain what a resource template does and what RBAC it needs; wire an output parameter or artifact from one step into the next; convert a workflow into a WorkflowTemplate and fire it from a CronWorkflow; and read a failing run with argo get, argo logs, and kubectl describe wf. Above all, hold the boundary line: Tekton is CI/CD pipelines; Argo Workflows is general orchestration of Kubernetes resources — and Argo CD, Argo Rollouts, and Argo Workflows are three different projects.

The documentation allowlist — read this twice

⚠ You cannot open argoproj.github.io during the exam

During the CNPE exam the only documentation you may open is kubernetes.io/docs, kubernetes.io/blog, any task-specific docs explicitly linked from the exam’s Quick Reference box, and local man//usr/share docs on the exam terminal. Argo Workflows’ own documentation site is not on that list — no copying the DAG example, no looking up retryPolicy values. What you can lean on: kubectl explain workflow.spec against the installed CRDs, argo --help and per-subcommand help, argo lint before you submit, and examples already present in the task environment. Practise with the docs closed at least once. The manifests worth memorising are on Know Cold.

⚖ CNPA vs CNPE — That allowlist is a CNPE mechanic: CNPE is hands-on, and permits those narrow lookups mid-task. CNPA is stricter, not looser — a fully closed-book, multiple-choice exam with zero external references of any kind, not even kubernetes.io. Knowing Argo Workflows' CRDs and template types cold is still worth having for CNPA's closed-book recall.

Where to go next on this site — and officially

Continue with CI/CD & Progressive Delivery, the primary lesson this tool serves; compare against Tekton; see how the finished artefact gets deployed in GitOps Workflows and Argo CD; and browse the landscape from the tool hub. Unfamiliar words are in the Glossary; debugging moves in Triage: workloads. Officially — outside the exam — trust the project docs at argo-workflows.readthedocs.io, the examples directory and field reference at github.com/argoproj/argo-workflows, and the Argo home at argoproj.github.io. That examples directory is the fastest way to learn the template vocabulary: a hundred small runnable workflows, one idea each.

🐢 Timmy’s checkpoint

1. Name the four Argo Workflows custom resources you would author yourself, and what each one is for. 2. What is the structural difference between a steps template and a dag template? 3. What does a resource template do, and what two categories of RBAC does a workflow ServiceAccount typically need? 4. Which two spec fields clean up finished pods and finished Workflow objects — and which does which? 5. What is the difference between argo stop and argo terminate? 6. In one sentence, how would you tell a colleague when to reach for Tekton instead of Argo Workflows?

Check your answers
  1. Workflow — one execution, definition plus live status. WorkflowTemplate — a reusable namespaced definition that doesn’t run by itself. ClusterWorkflowTemplate — the same, cluster-scoped, for platform-published golden pipelines. CronWorkflow — a schedule wrapped around a workflow spec.
  2. steps is a list of lists: the outer list runs sequentially, the inner list runs in parallel. dag is a flat set of tasks each declaring dependencies, and the engine runs anything whose dependencies are satisfied — maximum parallelism, arbitrary graph shape.
  3. A resource template performs create/apply/delete/patch/get on an arbitrary Kubernetes object and can block on a successCondition. The ServiceAccount needs (a) create+patch on workflowtaskresults for the executor on every workflow, and (b) explicit verbs on the target kind — including get/list/watch if a successCondition is used.
  4. podGC.strategy deletes the step pods; ttlStrategy (secondsAfterCompletion / secondsAfterSuccess / secondsAfterFailure) deletes the Workflow object. You want both, and log archiving if you GC pods aggressively.
  5. argo stop is graceful and still runs exit handlers; argo terminate kills immediately and skips them, which can orphan anything the workflow provisioned.
  6. Reach for Tekton when the job is specifically CI/CD — build, test, scan, sign, publish — and you want its Task/Pipeline catalogue; reach for Argo Workflows when you need one general orchestrator for many shapes of multi-step work, including acting directly on Kubernetes objects.