Finally Tasks in Kestra – Always-Run Cleanup

Define a block of tasks that always run at the end of a flow, regardless of task status.

Finally tasks – always-run cleanup

finally tasks are useful for cleanup operations that must run at the end of your flow, whether the execution ends in success or failure.

finally component

finally is a block of tasks that execute at the end of your workflow, regardless of the status of prior tasks. This ensures cleanup or teardown steps always occur, no matter how the flow ends.

For example, you might use a finally block to turn off a cloud service when the flow finishes, regardless of the outcome.

finally vs errors

finally and errors can both run near the end of a flow, but they are meant for different jobs.

  • Use finally for cleanup and teardown that must happen every time.
  • Use errors for failure-specific handling such as alerts, remediation, or fallback actions.

Unlike errors, finally is not tied to a failure path. It runs whether the flow succeeds or fails, and it runs while the execution is still in the RUNNING state.

For failure-specific handling, including local handlers inside flowable tasks, see the errors documentation. For post-run actions based on the final execution state, see the afterExecution documentation.

finally example

In the example below, one task is designed to fail, and an errors task logs a message to signal the failure. The finally task still runs after the other tasks finish. Here it logs another message, but in practice it could be used to shut down resources started for the flow.

id: finally_example
namespace: company.team
tasks:
- id: fail
type: io.kestra.plugin.core.execution.Fail
errorMessage: Test downstream tasks
errors:
- id: send_alert
type: io.kestra.plugin.core.log.Log
message: alert on failure
finally:
- id: cleanup_task
type: io.kestra.plugin.core.log.Log
message: cleaning up resources

If you change the example so the first task succeeds, as shown below, the finally task still runs in the same way:

id: finally_example
namespace: company.team
tasks:
- id: log
type: io.kestra.plugin.core.log.Log
errorMessage: "This flow executes successfully!"
errors:
- id: send_alert
type: io.kestra.plugin.core.log.Log
message: alert on failure
finally:
- id: cleanup_task
type: io.kestra.plugin.core.log.Log
message: cleaning up resources

As in the first example, the finally task runs at the end even though the errors task does not send an alert, ensuring cleanup still happens regardless of status.

Beyond simple cleanup, finally can manage external services. For example, you might spin up Redis, Elasticsearch, or Kafka to run queries or QA checks, and then ensure the service is stopped when the flow ends. The following example shows how to start a Redis Docker container, run some database operations, and then stop the container when the flow finishes.

id: dockerRedis
namespace: company.team
variables:
host: host.docker.internal
tasks:
- id: start
type: io.kestra.plugin.docker.Run
containerImage: redis
wait: false
portBindings:
- "6379:6379"
- id: sleep
type: io.kestra.plugin.core.flow.Sleep
duration: PT1S
description: Wait for the Redis container to start
- id: set
type: io.kestra.plugin.redis.string.Set
url: "redis://:redis@{{vars.host}}:6379/0"
key: "key_string_{{execution.id}}"
value: "{{flow.id}}"
serdeType: STRING
- id: get
type: io.kestra.plugin.redis.string.Get
url: "redis://:redis@{{vars.host}}:6379/0"
key: "key_string_{{execution.id}}"
serdeType: STRING
- id: assert
type: io.kestra.plugin.core.execution.Assert
errorMessage: "Invalid get data {{outputs.get}}"
conditions:
- "{{outputs.get.data == flow.id}}"
- id: delete
type: io.kestra.plugin.redis.string.Delete
url: "redis://:redis@{{vars.host}}:6379/0"
keys:
- "key_string_{{execution.id}}"
- id: getAfterDelete
type: io.kestra.plugin.redis.string.Get
url: "redis://:redis@{{vars.host}}:6379/0"
key: "key_string_{{execution.id}}"
serdeType: STRING
- id: assertAfterDelete
type: io.kestra.plugin.core.execution.Assert
errorMessage: "Invalid get data {{outputs.getAfterDelete}}"
conditions:
- "{{(outputs.getAfterDelete contains 'data') == false}}"
finally:
- id: stop
type: io.kestra.plugin.docker.Stop
containerId: "{{outputs.start.taskRunner.containerId}}"

Was this page helpful?