Guarantee Cleanup at Flow End with Finally Tasks
For the complete documentation index, see llms.txt. For a full content snapshot, see llms-full.txt. Append.mdto anykestra.io/docs/*URL for plain Markdown.
The finally block defines tasks that always run at the end of a flow, whether the execution succeeds, fails, or is killed.
finally vs errors
finally and errors both run near the end of a flow, but serve different purposes:
finally runs regardless of execution outcome; errors runs only when a task or the flow fails.
finally tasks run while the execution is still RUNNING. If you need to react to a specific terminal state (SUCCESS, FAILED, etc.), use afterExecution instead. For failure-specific handling including local handlers inside flowable tasks, see the errors documentation.
Examples
Failure path example
One task is designed to fail and the errors block sends an alert. The finally task runs regardless: here it logs a message, but in practice it would shut down any resources the flow started:
id: finally_examplenamespace: 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 resourcesSuccess path example
When the task succeeds, errors is skipped but finally still runs:
id: finally_examplenamespace: company.team
tasks: - id: log type: io.kestra.plugin.core.log.Log message: "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 resourcesManaging an external service
Use finally to guarantee a service is stopped even if the flow fails mid-run. The following flow starts a Redis container, runs operations against it, and stops the container in the finally block:
id: dockerRedisnamespace: 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?