Quickstart
Run one task
from ttasks import Task, TaskExecutor
executor = TaskExecutor()
task = Task.bash("echo hello", title="Say hello")
result = executor.execute(task)
assert task.is_done
assert result.output == "hello\n"
assert task.result is result
TaskExecutor registers built-in handlers for Bash, PowerShell, Copilot prompt,
and Copilot agent tasks. You can override any handler or create an executor with
no defaults by using TaskExecutor.empty().
Subscribe to events
Every executor has an event bus for lifecycle, progress, and output events. Use
subscribe(...) for long-lived observers and call the returned function when you
are done:
from ttasks import Task, TaskEvent, TaskExecutor
def print_event(event: TaskEvent) -> None:
print(f"{event.type.value}: {event.task.title} -> {event.status.value}")
executor = TaskExecutor()
unsubscribe = executor.events.subscribe(print_event)
try:
executor.execute(Task.bash("echo hello", title="Say hello"))
finally:
unsubscribe()
For scoped subscriptions in tests or short blocks, use
with executor.events.subscribed(callback): .... See
Progress and output for progress events,
streamed subprocess output, and event payload details.
Share a Copilot agent session
The default Copilot agent handler creates a fresh Copilot session for each task.
Use CopilotAgentSession when multiple Task.agent(...) tasks should share one
conversation:
from ttasks import CopilotAgentSession, Task, TaskExecutor, TaskType
with CopilotAgentSession(working_directory="/path/to/repo") as agent:
executor = TaskExecutor()
executor.register(TaskType.AGENT, agent.handler())
executor.execute(Task.agent("Create a first change."))
executor.execute(Task.agent("Continue from the previous change."))
Shared sessions preserve conversation state across agent tasks. The handler
serializes turns through the session, including when used by TaskGraph.
Run a graph
from ttasks import Task, TaskExecutor, TaskGraph
build = Task.bash("echo build", title="Build")
test = Task.bash("echo test", title="Test")
package = Task.bash("echo package", title="Package")
graph = TaskGraph(title="build pipeline")
graph.add(build)
graph.add(test, after=[build])
graph.add(package, after=[test])
graph.run(TaskExecutor())
assert graph.ok
assert graph.succeeded == [build, test, package]
If a task fails or is cancelled, downstream tasks are marked blocked and are not submitted. Use finally tasks for cleanup and reporting work that should still run after failure.
Persist tasks and graphs
Use InMemoryStore when you want an explicit store for tests or short-lived
programs. Use SQLiteStore when you want task and graph state to survive process
restarts.
from pathlib import Path
from ttasks import InMemoryStore, SQLiteStore, Task, TaskExecutor, TaskGraph
# Swap this for InMemoryStore() when durability is not needed.
store = SQLiteStore(Path("ttasks.db"))
executor = TaskExecutor(store=store)
build = Task.bash("echo build", title="Build")
test = Task.bash("echo test", title="Test")
graph = TaskGraph(title="stored pipeline")
graph.add(build)
graph.add(test, after=[build])
graph.run(executor)
assert store.tasks[build.id].status == build.status
assert store.graphs[graph.id].ok is True
memory_store = InMemoryStore()
memory_store.tasks.save(Task.bash("echo scratch", title="Scratch"))
When an executor has a store, it saves tasks on lifecycle transitions and saves
graphs when TaskGraph.run(...) completes. SQLiteStore reads return detached
snapshots, so save changed objects again if you mutate them after loading.
Run the full demo
The repository includes main.py, a runnable example that combines a
SQLiteStore, task events, three graphs, graph outcome views, and stored graph
inspection:
uv run python main.py
The demo includes Copilot prompt/agent tasks, so it needs Copilot authentication for those steps.
Build docs locally
uv run mkdocs build --strict --site-dir site
uv run pdoc ttasks --output-directory site/api