Finally tasks
Finally tasks are graph-scheduling semantics for work that should run after one or more upstream tasks become inactive. They are useful for cleanup, reporting, artifact collection, and recommendations.
They are not Python finally blocks, and they are not graph-level retry. They
are regular tasks with special readiness rules inside TaskGraph.
Basic form
from ttasks import Task, TaskGraph
lint = Task.bash("ruff check .", title="Lint")
test = Task.bash("pytest", title="Tests")
report = Task.bash("python scripts/report.py", title="Write report")
graph = TaskGraph(title="preflight")
graph.add(lint)
graph.add(test)
graph.add(report, after=[lint, test], finally_=True)
The finally task becomes ready once every listed after task is no longer
active, even if one failed, was cancelled, or became blocked.
Cleanup after failed work
Use a required finally task for cleanup that must succeed for the graph to be healthy.
build = Task.bash("make build", title="Build")
cleanup = Task.bash("rm -rf .tmp-build", title="Clean temporary files")
graph.add(build)
graph.add(cleanup, after=[build], finally_=True)
If build fails, cleanup still runs. If cleanup fails, it is a required
task by default, so graph.ok is false.
Reports and artifacts
A finally task receives the listed after tasks through context.upstream, just
like a normal dependency. Use that to summarize results or collect artifact
paths.
def write_report(context):
lines = []
for upstream in context.upstream.values():
result = upstream.result
lines.append(f"{upstream.title}: {upstream.status.value}")
if result and result.error:
lines.append(result.error)
return "\n".join(lines)
Only direct dependencies are included. If a report needs an earlier ancestor, add that ancestor as an explicit dependency.
Optional recommendations
Use required=False for best-effort reporting, artifact collection, or Copilot
recommendation tasks. Their failures are visible, but they do not make
graph.ok false by themselves.
recommend = Task.prompt(
"Summarize preflight output and recommend the next action.",
title="Copilot recommendation",
)
graph.add(
recommend,
after=[lint, test, report],
finally_=True,
required=False,
)
This is useful when the primary work should remain authoritative, but optional diagnostics should still appear in the run summary.
Required vs optional
| Setting | Failure effect | Good use |
|---|---|---|
required=True |
Failure makes graph.ok false |
Cleanup that must complete |
required=False |
Failure is reported but does not make graph.ok false |
Reports, artifact collection, AI recommendations |
Required and optional finally tasks are available through introspection views:
graph.finally_tasksgraph.optional_tasksgraph.required_tasksgraph.optional_failedgraph.required_failedgraph.required_blocked
graph.ok remains the authoritative success predicate.
Runnable example
The repository includes a local deterministic example:
uv run python examples/finally_tasks.py
It demonstrates failed primary work, cleanup that still runs, report generation, and an optional recommendation task whose failure is visible without changing the required outcome.