Kanban
Persistent task board for multi-agent coordination
Kanban enables agents to coordinate through persistent tasks, creating a shared workspace where work is tracked and distributed across multiple agents.
from praisonaiagents import Agent
coordinator = Agent(name="Coordinator", instructions="Break work into kanban tasks")
coordinator.start("Plan the auth feature and assign workers")The user asks for a feature; the coordinator creates tasks workers pick up from the shared board.
Quick Start
- Create Agent with Kanban Tools
python from praisonaiagents import Agent from praisonaiagents.kanban import KanbanStoreProtocol # Agent with kanban protocols (implementation needed from wrapper) agent = Agent(name="Coordinator", instructions="Break tasks down") result = agent.start("Create user auth system") - Add Worker Agent
python from praisonaiagents import Agent from praisonaiagents.kanban import VALID_KANBAN_STATUSES worker = Agent(name="Worker", instructions="Claim and complete tasks") result = worker.start("Find ready tasks")
How It Works
Task coordination happens through a SQLite-backed persistent store that all agents and the UI share.
| Component | Purpose |
|---|---|
| Kanban Store | SQLite database storing tasks, comments, links |
| Agent Tools | 8 functions for task CRUD operations |
| CLI Commands | Human interface for task management |
| Background Dispatcher | Auto-claims ready tasks for processing |
Concepts
Task Statuses
Tasks flow through 8 defined states from creation to completion:
Boards
Boards provide workspace isolation for different projects or contexts:
DAG Links
Tasks form directed acyclic graphs through parent-child dependencies:
Claim/Release
Workers coordinate through atomic claim and release operations:
Agent Tools
Kanban tools are available through the SDK protocols. The wrapper implementation provides these agent tools:
Task Management
| Tool | Purpose | Example |
|---|---|---|
kanban_create | Create new task | kanban_create("Implement auth", assignee="dev", idempotency_key="auth-v1") |
kanban_list | Filter tasks | kanban_list(status="ready", assignee="dev") |
kanban_show | Get task details | kanban_show("task_abc123") |
kanban_runs | Read attempt history | kanban_runs("task_abc123") |
Status Changes
| Tool | Purpose | Example |
|---|---|---|
kanban_complete | Mark task done with structured handoff | kanban_complete("task_abc123", summary="Auth done; 14 tests pass", metadata={"changed_files": ["auth.py"]}) |
kanban_block | Block with reason | kanban_block("task_abc123", "Need API keys") |
Coordination
| Tool | Purpose | Example |
|---|---|---|
kanban_comment | Add progress note | kanban_comment("task_abc123", "50% complete") |
kanban_link | Create dependency | kanban_link("design_task", "implement_task") |
kanban_heartbeat | Signal liveness | kanban_heartbeat("task_abc123", "testing") |
Boards & Storage
Single Board (Default)
~/.praisonai/kanban.dbMulti-Board Layout
~/.praisonai/kanban/boards/
├── project-a/kanban.db
├── project-b/kanban.db
└── team-x/kanban.dbEnvironment Configuration
| Variable | Effect | Example |
|---|---|---|
PRAISONAI_KANBAN_BOARD | Select active board | export PRAISONAI_KANBAN_BOARD=project-a |
PRAISONAI_KANBAN_DB | Override DB path | export PRAISONAI_KANBAN_DB=/custom/path.db |
Attempt History & Retry
Each kanban task records every attempt, auto-blocks after repeated failures, and hands off a structured summary on completion.
- Idempotent create with retry limit
python from praisonaiagents import Agent from praisonai.tools.kanban_tools import kanban_create agent = Agent(name="Coordinator", instructions="Break work into tasks") task = kanban_create( "Replace bcrypt with argon2", body="Migrate auth module to argon2", assignee="coder", max_retries=3, # per-task circuit-breaker idempotency_key="auth-refactor-q3", # safe to repeat ) - Structured completion handoff
python from praisonai.tools.kanban_tools import kanban_complete kanban_complete( task["id"], summary="argon2 migration; 14 tests pass; CHANGELOG updated", metadata={ "changed_files": ["auth.py", "tests/test_auth.py"], "tests_run": 14, "residual_risk": "rotate old hashes on next login", }, ) - Read attempt history on retry
python from praisonai.tools.kanban_tools import kanban_runs history = kanban_runs(task["id"]) for run in history["runs"]: print(run["outcome"], run["error"] or run["summary"]) # crashed: ModuleNotFoundError: argon2 # completed: argon2 migration; 14 tests pass; CHANGELOG updated
What the dispatcher does automatically
When a worker claims a task, the dispatcher opens a run with the claim — anything the worker writes via kanban_complete closes it as completed, anything that crashes closes it as crashed/failed. Below the max_retries limit, failed attempts release the claim and put the task back on ready for re-dispatch. At the limit, the task auto-blocks with gave_up so a human can step in. A successful completion resets the failure counter — the limit is consecutive, not cumulative.
Non-blocking git. With per-task worktree isolation (workspace_kind="worktree" or repo_path), every git command the dispatcher runs — fork, merge, cleanup — happens off the gateway event loop, so chat and WebSocket traffic stay responsive during a slow merge. Integrations into the shared repo are serialised, so two tasks never run git merge at the same time. If the dispatcher stops mid-dispatch, a half-prepared worktree and its branch are removed so a retry can recreate them.
New params on kanban_create
| Option | Type | Default | Description |
|---|---|---|---|
max_retries | int | 3 (board default) | Per-task circuit-breaker limit. After this many consecutive failed attempts the task auto-blocks. Invalid values (<1 / non-numeric) fall back to the board default. |
idempotency_key | str | None | Per-board, per-tenant dedup key. Repeating a create with the same key returns the existing task instead of a duplicate — safe for retrying automation and webhooks. |
New params on kanban_complete
| Option | Type | Default | Description |
|---|---|---|---|
summary | str | "" | Structured summary of what was done. Surfaced to linked children and retrying workers. |
metadata | dict | {} | Structured handoff fields. Common keys: changed_files, tests_run, residual_risk, next_steps. |
comment | str | "" | Free-text completion comment (kept for back-compat). |
New tool: kanban_runs
| Tool | Purpose | Example |
|---|---|---|
kanban_runs | Read attempt history (oldest first) | kanban_runs("task_abc123") → {"runs": [{"outcome": "crashed", "error": "...", "started_at": "...", "ended_at": "..."}, ...], "count": 2} |
Each row matches the KanbanRunProtocol shape:
| Field | Type | Description | |
|---|---|---|---|
id | int | Auto-incremented run ID | |
task_id | str | Parent task ID | |
profile | str | Worker/profile identifier | |
outcome | `str \ | None` | completed, blocked, crashed, failed, or gave_up; None while the run is open |
summary | str | Structured handoff summary | |
metadata | dict | Structured handoff fields | |
error | str | Error text for failed/crashed attempts | |
started_at | `str \ | None` | ISO-8601 timestamp |
ended_at | `str \ | None` | ISO-8601 timestamp; None while the run is open |
New Task fields
| Field | Type | Meaning | |
|---|---|---|---|
max_retries | `int \ | None` | Per-task circuit-breaker limit. None means use the board default (3). |
consecutive_failures | int | Count of consecutive failed attempts since the last completed outcome. Reset to 0 by a successful completion. | |
current_run_id | `int \ | None` | Active attempt id; None when no run is open. |
Common Patterns
Automation-safe creation — pass idempotency_key derived from the trigger (f"github-issue-{n}", f"webhook-{uuid}"). Re-firing the webhook is a no-op instead of spawning duplicates. Idempotency is per-board, per-tenant — two tenants on the same board never collide on the same key.
Long-running flaky network tasks — set max_retries=5 per task to absorb transient flakes without giving up too early. Watch consecutive_failures in kanban_show to spot stuck tasks.
Retrying workers use prior failures — call kanban_runs(task_id) at the start of a retry attempt to read prior outcomes and errors. Use summary and metadata from the most recent failed run as context: “previous attempt crashed at line X; avoid path Y.”
Use idempotency_key for every automated kanban_create
Webhooks, schedulers, and GitHub-issue triagers should always pass an idempotency_key. The cost is one extra string; the value is zero duplicate tasks under retry.
Tune max_retries per task type
Fast deterministic work: max_retries=1. Flaky network or integration tests: max_retries=3–5. Leave the rest at the board default (3).
Send a real summary and metadata
Use summary and metadata in kanban_complete instead of free-text comments. Downstream child tasks and retrying workers consume the structured fields; humans still read them fine.
Read kanban_runs before retrying
The structured run history is the cheapest “what went wrong last time” context a retrying worker can fetch. Call kanban_runs(task_id) at the start of every retry attempt.
Common Patterns
Coordinator-Worker Pattern
# Coordinator breaks down requests
from praisonaiagents import Agent
from praisonaiagents.kanban import VALID_KANBAN_STATUSES
coordinator = Agent(
name="Coordinator",
instructions="Break user requests into kanban tasks"
)
# Worker with heartbeat reporting
worker = Agent(
name="Worker",
instructions="Claim ready tasks, report progress via heartbeat"
)Background Processing
# Background processing requires wrapper implementation
# The praisonaiagents.kanban protocols support:
# - Task claiming and status updates
# - Heartbeat reporting for long-running tasks
# - Multi-board coordinationWorker with Heartbeat
from praisonaiagents import Agent
from praisonaiagents.kanban import VALID_KANBAN_STATUSES
worker = Agent(
name="Worker",
instructions="""Claim ready tasks and report liveness via heartbeat.
Use kanban_heartbeat every 30 seconds during long-running work."""
)
# Worker claims task and reports progress
result = worker.start("Find ready tasks, claim one, and report progress")Human-Agent Collaboration
# Human-agent collaboration pattern
# Requires wrapper implementation of:
# - CLI commands for task management
# - UI board visualization
# - Agent-to-store protocol bindingsRuns, Retries, and Structured Handoff
Kanban now tracks every execution attempt for each task via the task_runs table, supports per-task retry limits, and allows agents to pass structured data when completing a task.
kanban_complete — structured handoff
kanban_complete(
task_id="task-123",
summary="Implemented OAuth2 flow with PKCE",
metadata={"pr_url": "https://github.com/org/repo/pull/42", "test_coverage": 0.91},
)| Parameter | Type | Default | Description |
|---|---|---|---|
task_id | str | _(required)_ | Task to complete |
summary | str | "" | Human-readable completion summary stored in the run record |
metadata | dict | {} | Arbitrary structured data for downstream consumers (e.g. PR URL, metrics) |
kanban_create — idempotency and retries
kanban_create(
title="Write unit tests for auth module",
body="Cover all edge cases in JWT validation",
idempotency_key="auth-tests-v1",
max_retries=3,
)| Parameter | Type | Default | Description | |
|---|---|---|---|---|
title | str | _(required)_ | Task title | |
body | str | "" | Task description | |
idempotency_key | `str \ | None` | None | If set, calling kanban_create again with the same key returns the existing task instead of creating a duplicate |
max_retries | int | 0 | Max number of times the dispatcher retries a failed run. After N failures the task is auto-moved to blocked |
kanban_runs — per-task run history
List all execution attempts for a task:
runs = kanban_runs(task_id="task-123")
for run in runs:
print(run["outcome"], run["summary"], run["created_at"])Each run record contains:
| Field | Type | Description | |
|---|---|---|---|
id | str | Unique run ID | |
task_id | str | Parent task | |
outcome | str | "success", "failure", or "in_progress" | |
summary | str | Summary provided at completion | |
metadata | dict | Structured handoff data | |
created_at | float | Unix timestamp of run start | |
finished_at | `float \ | None` | Unix timestamp of run end |
Auto-block after max retries
When max_retries > 0 and the task accumulates that many "failure" run outcomes, the dispatcher automatically moves the task from running to blocked. The task must be manually unblocked (kanban_move(task_id, "ready")) after the root cause is resolved.
Per-Task Worktree Isolation
Give every kanban worker its own git worktree so parallel tasks never overwrite each other’s files.
from praisonaiagents import Agent
from praisonai.tools.kanban_tools import kanban_create
# Opt this task into per-task worktree isolation
task = kanban_create(
"Refactor auth module",
workspace_kind="worktree", # <-- the whole feature
)One keyword moves a task onto its own branch and working directory. Everything else — creation, merge-back, and cleanup — happens automatically.
Auto-upgrade for repo-linked tasks
Link a task to a git repo and the dispatcher gives it a worktree automatically — no keyword needed.
from praisonai_bot.kanban.sqlite_store import SQLiteKanbanStore
store = SQLiteKanbanStore()
# repo_path alone opts the task into isolation — workspace_kind is set for you
task = store.create_task({
"title": "Refactor auth module",
"repo_path": "/repos/service-a",
})
assert task.workspace_kind == "worktree"
assert task.branch == f"kanban/{task.id}"
assert task.metadata["repo_path"] == "/repos/service-a"The upgrade is triggered by linkage, not by task content — the task body is never inspected.
| Task input | Effective workspace_kind | Effective branch | Notes |
|---|---|---|---|
repo_path="/repos/svc" (git repo) | worktree | kanban/<task_id> | Auto-upgrade fires. metadata["repo_path"] is persisted so dispatch sees the same linkage. |
metadata={"repo_path": "/repos/svc"} | worktree | kanban/<task_id> | Same — repo_path can live inside metadata too. |
repo_path="/repos/svc" + workspace_kind="default" | default | (unset) | Explicit kind always wins. Deliberate opt-out. |
repo_path="/repos/svc" + auto_worktree=False | default | (unset) | Board- or task-level escape hatch. metadata["auto_worktree"]=False works too. |
workspace_kind="worktree" (no repo_path) | worktree | kanban/<task_id> | Branch is derived at create time, not dispatch. |
repo_path="/not/a/repo" | default | (unset) | Bad path → not a repo → no upgrade. Never fails the create. |
repo_path="/repos/svc" + metadata={"repo_path": "/explicit/other"} | worktree | kanban/<task_id> | Explicit metadata.repo_path is respected — top-level repo_path does not overwrite. |
Disable the upgrade per task (or per board via metadata) with auto_worktree=False:
from praisonai_bot.kanban.sqlite_store import SQLiteKanbanStore
store = SQLiteKanbanStore()
# repo-linked, but isolation explicitly disabled
task = store.create_task({
"title": "Read-only audit",
"repo_path": "/repos/service-a",
"auto_worktree": False,
})
assert task.workspace_kind == "default"How It Works
The dispatcher forks a worktree from the base branch, runs the worker inside it, then merges the branch back on success.
On success the dispatcher auto-commits any uncommitted worker edits as kanban worker output for kanban/<task_id>, merges the branch into the base with git merge --no-ff, then tears the worktree down. On conflict the merge is aborted, so the base branch is never left half-merged.
Choosing default vs worktree
Pick isolation only when workers actually edit files in a git repo.
New Task fields
| Field | Type | Default | Description | |
|---|---|---|---|---|
workspace_kind | str | "default" | "worktree" opts the task into per-task git worktree isolation. Any other value keeps the shared-cwd behaviour. | |
repo_path | `str \ | None` | None | Filesystem path of the git repo this task is linked to. When set and the caller left workspace_kind unset, the store upgrades workspace_kind to "worktree" and persists the resolved path into metadata["repo_path"]. Non-git or unreadable paths are treated as unlinked (never fails the create). Also accepted inside metadata. |
auto_worktree | bool | True | Set to False to disable the repo-linked auto-upgrade for this task (or a whole board via metadata["auto_worktree"]). Explicit workspace_kind always wins over both this flag and repo_path. | |
branch | `str \ | None` | None | Populated at create_task time on worktree tasks (auto-upgraded or explicit workspace_kind="worktree") — kanban/<task_id>. An explicit branch in task_data is respected. Read it via kanban_show. |
worktree_path | `str \ | None` | None | Populated by the dispatcher — filesystem path of the worktree (~/.praisonai/kanban/worktrees/<task_id>). |
Environment configuration
Add these to the Environment Configuration table when the dispatcher runs outside the repo.
| Variable | Effect | Example |
|---|---|---|
PRAISONAI_KANBAN_REPO_DIR | Base repo path for worktree / merge operations | export PRAISONAI_KANBAN_REPO_DIR=/repos/service-a |
PRAISONAI_KANBAN_BASE_BRANCH | Fallback branch to fork from / integrate into when HEAD cannot be detected | export PRAISONAI_KANBAN_BASE_BRANCH=develop |
PRAISONAI_KANBAN_GIT_TIMEOUT | Total seconds allowed for any single git command the dispatcher runs (worktree add, merge, status, rev-parse). Timing out returns returncode=124 and a diagnosis in stderr — the dispatcher treats that as failure exactly like any other non-zero exit. See Subprocess Guard. | export PRAISONAI_KANBAN_GIT_TIMEOUT=600 |
The dispatcher branches from the current HEAD of PRAISONAI_KANBAN_REPO_DIR (defaulting to the process working directory). When HEAD cannot be detected, it falls back to PRAISONAI_KANBAN_BASE_BRANCH (default main).
Merge conflict handling
A conflict never touches the base branch — it routes the task to blocked for a human to resolve.
- The merge is aborted; the base branch is left untouched.
- The task moves to
blockedwith a comment listing the conflicted files — read it viakanban_show(task_id). - The
KANBAN_TASK_BLOCKEDhook fires; the conflicted files live on the task comment thread, not the hook payload. - The worktree is preserved on disk for inspection.
To recover, resolve the conflicts inside the kept worktree, then re-run or discard it:
from praisonai.tools.kanban_tools import kanban_move
# After resolving conflicts inside ~/.praisonai/kanban/worktrees/<task_id>
kanban_move("task_abc123", "ready") # re-dispatch the taskOr discard the worktree entirely from the base repo:
git worktree remove --force ~/.praisonai/kanban/worktrees/task_abc123Lossless-only worktree teardown
The dispatcher never destroys work a worker produced but failed to integrate — a worktree with uncommitted changes or unmerged commits is preserved on disk with a worktree_preserved comment on the task.
sequenceDiagram
participant Dispatcher
participant Guard as Lossless guard
participant Git
participant Store as Kanban Store
Dispatcher->>Git: git merge --no-ff kanban/<id>
Dispatcher->>Guard: _remove_worktree(path, branch)
Guard->>Git: git status --porcelain
Guard->>Git: git rev-list --count base..branch
alt Clean tree and 0 commits ahead
Guard->>Git: git worktree remove --force
alt remove succeeded
Git-->>Guard: ok
Guard-->>Dispatcher: None (removed)
Dispatcher->>Store: drop tracking entry
else remove failed (nonzero / exception)
Git-->>Guard: error
Guard-->>Dispatcher: "worktree removal failed: …"
Dispatcher->>Store: add_comment worktree_preserved at <path>: …
Note over Dispatcher: tracking entry kept — no orphan
end
else Dirty or ahead of base
Guard-->>Dispatcher: reason string
Dispatcher->>Store: add_comment worktree_preserved at <path>: <reason>
Note over Dispatcher: worktree left on disk; tracking entry kept
endEvery preserved worktree records its reason as a task comment, readable via kanban_show(task_id):
| Reason (recorded on the task) | Trigger | Recovery |
|---|---|---|
worktree has uncommitted changes | Worktree still has files in git status --porcelain after integration | cd into the worktree, git add -A && git commit, then rerun cleanup — or discard with git -C <path> reset --hard HEAD then remove |
| `branch has |
commit(s) not in <base> | git rev-list –count <base>..<branch> > 0 (worker committed something that never merged into base) | Merge or cherry-pick the branch into base by hand, then rerun cleanup, or force-remove if the commits are known-throwaway | | could not read worktree status: <err> / could not compute ahead-count: <err> | The git status / git rev-list command itself failed | Investigate the worktree state manually — the guard errs toward preservation, so no data is at risk | | worktree removal failed: <detail> | git worktree remove –force returned nonzero | Read the git error, resolve, then rerun cleanup or force-remove | | worktree removal error: <exc> | git worktree remove` raised (permissions, missing binary, etc.) | Fix the underlying environment issue, then retry |
To recover a preserved worktree, commit anything worth keeping first, then integrate or remove it from the base repo:
# Read the preserved worktree, losslessly commit anything worth keeping
git -C ~/.praisonai/kanban/worktrees/task_abc123 status
git -C ~/.praisonai/kanban/worktrees/task_abc123 add -A
git -C ~/.praisonai/kanban/worktrees/task_abc123 commit -m "recover worker output"
# Then either integrate the branch by hand...
git merge kanban/task_abc123
# ...or, if the edits are known throwaway, remove the worktree manually
git worktree remove --force ~/.praisonai/kanban/worktrees/task_abc123Inside a dispatcher-owned code path (e.g. a custom KanbanDispatcher subclass), the equivalent destructive teardown is an explicit force=True:
# Explicitly opt into destructive teardown — bypasses the lossless guard
dispatcher._remove_worktree(path, branch, force=True)Best Practices
Enable worktree isolation only when workers edit files
Tasks that only call APIs or print reports gain nothing from isolation and pay the git overhead. Keep workspace_kind="default" for read-only work; reserve "worktree" for concurrent file edits.
Set PRAISONAI_KANBAN_REPO_DIR outside the repo
The dispatcher falls back to os.getcwd() at start, which is wrong under systemd/supervisord. Export PRAISONAI_KANBAN_REPO_DIR=/path/to/repo so worktrees and merges run against the right checkout.
Use short, git-ref-safe task IDs
The dispatcher rejects .., /, leading ., spaces, and other invalid chars. Unsafe IDs fail closed (no isolation, no worker) — keep IDs to [A-Za-z0-9._-].
Sweep stuck worktrees under ~/.praisonai/kanban/worktrees/
Both blocked-on-conflict tasks and clean-merge tasks with a worktree_preserved comment leave their worktree on disk by design. Read the comment for the reason (kanban_show <task_id>), integrate any recoverable work by hand, then git worktree remove --force ~/.praisonai/kanban/worktrees/<task_id> once resolved.
Expect serialised integration across concurrent worktree tasks
Workers run in parallel in their own worktrees; only the integration step (merge-back, cleanup) is serialised by an in-process lock. This is a safety guarantee, not throttling. If you run several dispatcher processes against the same repo, add your own file-level lock.
Prefer repo_path over workspace_kind for repo-scoped work
Linking a task to its repo (repo_path="/repos/foo") opts it into isolation and records the isolating repo in metadata so the dispatcher never loses the linkage. workspace_kind="worktree" still works for callers that already track the repo themselves.
Best Practices
Task Granularity
Create tasks that can be completed in 15-30 minutes. Break larger work into linked subtasks using kanban_link for proper dependency tracking.
Status Management
Move tasks through statuses systematically: todo → ready → running → done. Use blocked for dependencies and review for human approval.
Agent Coordination
Use kanban_heartbeat during long-running tasks to signal liveness. Add detailed comments with kanban_comment to track progress and decisions.
Board Organization
Use separate boards for different projects or contexts. Default board works well for single-project setups.