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 — making it the right place for cleanup and resource teardown.
finally vs errors
finally and errors both run near the end of a flow, but serve different purposes:
- Use
finallyfor cleanup and teardown that must happen every time, regardless of outcome. - Use
errorsfor failure-specific handling such as alerts, remediation, or fallback actions.
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
Cleanup after a failed task
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 resourcesCleanup after a successful task
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?