Argo Exit Handlers don’t actually run after a workflow has fully exited; they run when a workflow enters an exit state, which is a subtle but crucial distinction.

Let’s see this in action. Imagine a simple workflow that either succeeds or fails, and we want to ensure a cleanup step always runs.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: exit-handler-example-
spec:
  entrypoint: main
  templates:
    - name: main
      dag:
        tasks:
          - name: run-task
            template: run-task-template
          - name: fail-task
            template: fail-task-template
            when: "tasks.run-task.phase == Failed" # Only run if run-task fails

    - name: run-task-template
      container:
        image: alpine:latest
        command: ["sh", "-c"]
        args: ["echo 'Running main task' && exit 0"] # This task succeeds

    - name: fail-task-template
      container:
        image: alpine:latest
        command: ["sh", "-c"]
        args: ["echo 'Running fail task' && exit 1"] # This task fails

    - name: cleanup
      container:
        image: alpine:latest
        command: ["sh", "-c"]
        args: ["echo 'Cleaning up!'"]

  # This is where the magic happens
  exitHandlers:
    - name: always-run-cleanup
      when: "true" # Always true, so this handler always runs
      template: cleanup

When you submit this workflow, Argo will first execute the main DAG. The run-task will succeed. Since run-task succeeded, the fail-task (which has a when condition depending on run-task failing) will not run.

Now, here’s the key: the workflow has not yet exited. It has completed its defined entrypoint tasks. At this point, Argo evaluates the exitHandlers. Since the when: "true" condition on always-run-cleanup is met, the cleanup template is executed. After the cleanup template finishes, the workflow then truly exits.

If run-task were to fail, the fail-task would execute. After fail-task finishes (and thus the DAG completes its execution path), Argo would again evaluate the exitHandlers. always-run-cleanup would still run because its when condition is always true.

The problem this solves is ensuring critical post-execution logic, like resource deallocation, notifications, or final logging, happens regardless of whether the primary workflow logic succeeded or failed. The exitHandlers section is a top-level field in the Workflow spec, separate from the templates and entrypoint. This separation is what allows them to be triggered as a distinct phase of the workflow’s lifecycle, right before the final "Completed" or "Failed" status is set.

You can also specify conditions on exitHandlers using the when field, just like with tasks. For example, you could have a cleanup handler that only runs if the workflow failed:

  exitHandlers:
    - name: cleanup-on-failure
      when: "steps.main.status == Failed || tasks.run-task.status == Failed" # Example condition
      template: cleanup-on-failure-template

This allows for conditional cleanup logic. The when expression is evaluated against the status of the workflow and its tasks.

The most surprising thing about exit handlers is that their execution is tied to the entrypoint’s completion, not the workflow’s ultimate termination. They are a distinct execution phase that Argo guarantees to run before marking the workflow as fully done, provided their when condition evaluates to true. This means you can reliably run cleanup code even if your main workflow logic crashes unexpectedly.

The when condition on an exit handler can reference the status of any task or step defined within the workflow. You can access statuses like Succeeded, Failed, Skipped, Running, and Pending. For instance, tasks.my-task.status == Succeeded is a valid condition.

The trickiest part of exit handlers is understanding their execution context. They don’t run within a failed task; they run after the entrypoint has finished its execution path, whether that path ended in success or failure. This means that if an exit handler itself fails, it can cause the workflow to transition to a "Error" state, rather than "Failed," and the next exit handler might not even run if it depends on the prior one succeeding.

The next concept you’ll likely grapple with is orchestrating complex exit strategies using multiple, conditionally executed exit handlers, and how to handle failures within those exit handlers themselves.

Want structured learning?

Take the full Argo-workflows course →