The kanboard package provides a full-featured Python SDK for the
Kanboard JSON-RPC API.
- Installation
- Quick Start
- Client Initialization
- Resource Categories
- Resource Examples
- Projects
- Tasks
- Columns
- Swimlanes
- Comments
- Tags
- Users
- Subtasks
- Categories
- Board
- Groups and Group Members
- Links and Task Links
- External Task Links
- Files (Project and Task)
- Metadata (Project and Task)
- Project Permissions
- Actions
- Subtask Time Tracking
- Application Info
- Me (User API)
- Portfolios Plugin Resource
- Milestones Plugin Resource
- Exception Handling
- Batch API
- Low-Level
call() - Response Models
- Cross-Project Orchestration
pip install kanboard-cliThe kanboard SDK module is part of the kanboard-cli package.
from kanboard import KanboardClientfrom kanboard import KanboardClient
with KanboardClient(
url="https://kanboard.example.com/jsonrpc.php",
token="your-api-token",
) as kb:
# Create a project
project_id = kb.projects.create_project("My Project")
# Create a task
task_id = kb.tasks.create_task(
title="Implement feature X",
project_id=project_id,
color_id="green",
priority=2,
)
# List all active tasks
tasks = kb.tasks.get_all_tasks(project_id, status_id=1)
for task in tasks:
print(f"#{task.id} {task.title}")
# Add a comment
kb.comments.create_comment(task_id=task_id, user_id=1, content="Started work")
# Apply tags
kb.tags.set_task_tags(project_id, task_id, ["backend", "urgent"])Standard authentication using an API token. Suitable for machine clients and automation scripts.
from kanboard import KanboardClient
client = KanboardClient(
url="https://kanboard.example.com/jsonrpc.php",
token="your-api-token",
)The token is available in Kanboard at Settings → API.
Optional parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
url |
str |
(required) | Kanboard JSON-RPC endpoint URL |
token |
str |
"" |
API token for app auth mode |
timeout |
float |
30.0 |
Request timeout in seconds |
auth_mode |
str |
"app" |
Authentication mode ("app" or "user") |
username |
str | None |
None |
Username for user auth mode |
password |
str | None |
None |
Password or personal access token for user auth mode |
Required to access me.* endpoints. Uses HTTP Basic Auth with username and
password instead of the API token.
from kanboard import KanboardClient
client = KanboardClient(
url="https://kanboard.example.com/jsonrpc.php",
auth_mode="user",
username="admin",
password="your-password",
)
# Now me.* endpoints are available
me = client.me.get_me()
print(me.name)The recommended approach. The underlying HTTP connection is automatically
closed when the with block exits, even if an exception is raised.
from kanboard import KanboardClient
with KanboardClient(url="...", token="...") as kb:
projects = kb.projects.get_all_projects()
for p in projects:
print(p.name)
# HTTP client closed automatically hereWhen you manage the client lifetime yourself, call close() explicitly.
client = KanboardClient(url="...", token="...")
try:
tasks = client.tasks.get_all_tasks(1)
finally:
client.close()All resource methods are accessed through attributes of KanboardClient:
| Attribute | Resource Class | API Category |
|---|---|---|
kb.actions |
ActionsResource |
Automatic actions |
kb.application |
ApplicationResource |
Server info (version, timezone, colors, roles) |
kb.board |
BoardResource |
Board layout |
kb.categories |
CategoriesResource |
Task categories |
kb.columns |
ColumnsResource |
Board columns |
kb.comments |
CommentsResource |
Task comments |
kb.external_task_links |
ExternalTaskLinksResource |
External (URL) task links |
kb.group_members |
GroupMembersResource |
Group membership |
kb.groups |
GroupsResource |
User groups |
kb.links |
LinksResource |
Link type definitions |
kb.me |
MeResource |
Authenticated user (requires user auth) |
kb.milestones |
MilestonesResource |
Portfolio plugin milestone management (requires plugin) |
kb.portfolios |
PortfoliosResource |
Portfolio plugin portfolio management (requires plugin) |
kb.project_files |
ProjectFilesResource |
Project file attachments |
kb.project_metadata |
ProjectMetadataResource |
Project key-value metadata |
kb.project_permissions |
ProjectPermissionsResource |
Project user/group access |
kb.projects |
ProjectsResource |
Projects |
kb.subtask_time_tracking |
SubtaskTimeTrackingResource |
Subtask timers |
kb.subtasks |
SubtasksResource |
Subtasks |
kb.swimlanes |
SwimlanesResource |
Board swimlanes |
kb.tags |
TagsResource |
Tags |
kb.task_files |
TaskFilesResource |
Task file attachments |
kb.task_links |
TaskLinksResource |
Internal task-to-task links |
kb.task_metadata |
TaskMetadataResource |
Task key-value metadata |
kb.tasks |
TasksResource |
Tasks |
kb.users |
UsersResource |
Users |
with KanboardClient(url=URL, token=TOKEN) as kb:
# Create
project_id = kb.projects.create_project(
"My Project",
description="A sample project",
identifier="MYPROJ",
)
# Read
project = kb.projects.get_project_by_id(project_id)
print(project.name, project.identifier)
all_projects = kb.projects.get_all_projects()
# Update
kb.projects.update_project(project_id, name="Renamed Project")
# Enable / disable
kb.projects.disable_project(project_id)
kb.projects.enable_project(project_id)
# Activity feed
events = kb.projects.get_project_activity(project_id)
for event in events[:5]:
print(event)
# Remove
kb.projects.remove_project(project_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Create
task_id = kb.tasks.create_task(
title="Fix login bug",
project_id=1,
color_id="red",
priority=3,
date_due="2025-12-31",
tags=["bug", "auth"],
)
# Read
task = kb.tasks.get_task(task_id)
print(task.title, task.priority)
active_tasks = kb.tasks.get_all_tasks(project_id=1, status_id=1)
inactive_tasks = kb.tasks.get_all_tasks(project_id=1, status_id=0)
# Search using Kanboard filter syntax
results = kb.tasks.search_tasks(project_id=1, query="assignee:me status:open")
# Update
kb.tasks.update_task(task_id, title="Fixed login bug", priority=1)
# Close and reopen
kb.tasks.close_task(task_id)
kb.tasks.open_task(task_id)
# Move within project
kb.tasks.move_task_position(
project_id=1, task_id=task_id,
column_id=3, position=1, swimlane_id=0,
)
# Move to another project
kb.tasks.move_task_to_project(task_id, dest_project_id=2, column_id=1)
# Duplicate to another project
new_id = kb.tasks.duplicate_task_to_project(task_id, dest_project_id=2)
# Overdue tasks
overdue = kb.tasks.get_overdue_tasks()
project_overdue = kb.tasks.get_overdue_tasks_by_project(project_id=1)
# Remove
kb.tasks.remove_task(task_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Add a column
col_id = kb.columns.add_column(
project_id=1,
title="In Review",
task_limit=5,
description="Tasks waiting for review",
)
# Read
columns = kb.columns.get_columns(project_id=1)
column = kb.columns.get_column(col_id)
# Update
kb.columns.update_column(col_id, "Reviewed", task_limit=10)
# Reorder (1-based, leftmost = 1)
kb.columns.change_column_position(project_id=1, column_id=col_id, position=2)
# Remove
kb.columns.remove_column(col_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Add a swimlane
lane_id = kb.swimlanes.add_swimlane(
project_id=1,
name="High Priority",
description="Critical work items",
)
# Read
active = kb.swimlanes.get_active_swimlanes(project_id=1)
all_lanes = kb.swimlanes.get_all_swimlanes(project_id=1)
lane = kb.swimlanes.get_swimlane(lane_id)
lane_by_name = kb.swimlanes.get_swimlane_by_name(project_id=1, name="High Priority")
# Update
kb.swimlanes.update_swimlane(project_id=1, swimlane_id=lane_id, name="Critical")
# Enable / disable
kb.swimlanes.disable_swimlane(project_id=1, swimlane_id=lane_id)
kb.swimlanes.enable_swimlane(project_id=1, swimlane_id=lane_id)
# Reorder (1-based, topmost = 1)
kb.swimlanes.change_swimlane_position(project_id=1, swimlane_id=lane_id, position=1)
# Remove
kb.swimlanes.remove_swimlane(project_id=1, swimlane_id=lane_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Add a comment
comment_id = kb.comments.create_comment(
task_id=42,
user_id=1,
content="This looks good.",
)
# Read
comment = kb.comments.get_comment(comment_id)
all_comments = kb.comments.get_all_comments(task_id=42)
# Update
kb.comments.update_comment(comment_id, content="Revised review notes.")
# Remove
kb.comments.remove_comment(comment_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Create project-scoped tags
tag_id = kb.tags.create_tag(project_id=1, tag_name="urgent", color_id="red")
# Read
all_tags = kb.tags.get_all_tags()
project_tags = kb.tags.get_tags_by_project(project_id=1)
# Assign tags to a task (replaces existing)
kb.tags.set_task_tags(project_id=1, task_id=42, tags=["urgent", "backend"])
# Get tags on a task (returns dict: tag_id → tag_name)
task_tags = kb.tags.get_task_tags(task_id=42)
print(list(task_tags.values())) # ["urgent", "backend"]
# Update tag name
kb.tags.update_tag(tag_id, tag_name="critical")
# Remove
kb.tags.remove_tag(tag_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Create
user_id = kb.users.create_user(
username="jdoe",
password="s3cret",
name="John Doe",
email="[email protected]",
role="app-user",
)
# Read
user = kb.users.get_user(user_id)
user_by_name = kb.users.get_user_by_name("jdoe")
all_users = kb.users.get_all_users()
# Update
kb.users.update_user(user_id, name="Jonathan Doe", email="[email protected]")
# Enable / disable
kb.users.disable_user(user_id)
kb.users.enable_user(user_id)
active = kb.users.is_active_user(user_id) # bool
# Remove
kb.users.remove_user(user_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Create
subtask_id = kb.subtasks.create_subtask(
task_id=42,
title="Write unit tests",
user_id=1,
time_estimated=2.0,
status=0, # 0=todo, 1=in progress, 2=done
)
# Read
subtask = kb.subtasks.get_subtask(subtask_id)
all_subtasks = kb.subtasks.get_all_subtasks(task_id=42)
# Update
kb.subtasks.update_subtask(
subtask_id=subtask_id,
task_id=42,
status=1,
time_spent=0.5,
)
# Remove
kb.subtasks.remove_subtask(subtask_id)with KanboardClient(url=URL, token=TOKEN) as kb:
cat_id = kb.categories.create_category(project_id=1, name="Frontend", color_id="blue")
category = kb.categories.get_category(cat_id)
all_cats = kb.categories.get_all_categories(project_id=1)
kb.categories.update_category(cat_id, name="UI/UX")
kb.categories.remove_category(cat_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Returns a list of column dicts with nested swimlane/task structure
board = kb.board.get_board(project_id=1)
for column in board:
print(column["title"], "—", len(column.get("swimlanes", [])), "swimlanes")with KanboardClient(url=URL, token=TOKEN) as kb:
# Groups
group_id = kb.groups.create_group("Developers")
group = kb.groups.get_group(group_id)
all_groups = kb.groups.get_all_groups()
kb.groups.update_group(group_id, name="Engineering")
# Members
kb.group_members.add_group_member(group_id=group_id, user_id=3)
members = kb.group_members.get_group_members(group_id)
user_groups = kb.group_members.get_member_groups(user_id=3)
is_member = kb.group_members.is_group_member(group_id=group_id, user_id=3)
kb.group_members.remove_group_member(group_id=group_id, user_id=3)
kb.groups.remove_group(group_id)Link types define the vocabulary for task relationships ("blocks", "is blocked by", etc.).
with KanboardClient(url=URL, token=TOKEN) as kb:
# Create a bidirectional link type
link_id = kb.links.create_link(
label="blocks",
opposite_label="is blocked by",
)
opposite_id = kb.links.get_opposite_link_id(link_id)
# Look up
link = kb.links.get_link_by_id(link_id)
link_by_label = kb.links.get_link_by_label("blocks")
all_links = kb.links.get_all_links()
# Create a task-to-task link
task_link_id = kb.task_links.create_task_link(
task_id=10, opposite_task_id=20, link_id=link_id,
)
tl = kb.task_links.get_task_link_by_id(task_link_id)
all_task_links = kb.task_links.get_all_task_links(task_id=10)
kb.task_links.remove_task_link(task_link_id)
kb.links.remove_link(link_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Discover available link types
types = kb.external_task_links.get_external_task_link_types()
# Add an external URL link
ext_id = kb.external_task_links.create_external_task_link(
task_id=42,
url="https://github.com/org/repo/issues/10",
dependency="related",
type="weblink",
title="GitHub issue #10",
)
ext_link = kb.external_task_links.get_external_task_link(ext_id)
all_ext = kb.external_task_links.get_all_external_task_links(task_id=42)
kb.external_task_links.update_external_task_link(
ext_id, task_id=42, title="Updated title",
)
kb.external_task_links.remove_external_task_link(task_id=42, link_id=ext_id)Files are uploaded as base64-encoded blobs.
import base64
from pathlib import Path
with KanboardClient(url=URL, token=TOKEN) as kb:
# Project files
content = base64.b64encode(Path("report.pdf").read_bytes()).decode()
pf_id = kb.project_files.create_project_file(
project_id=1,
filename="report.pdf",
blob=content,
)
pf = kb.project_files.get_project_file(pf_id)
all_pf = kb.project_files.get_all_project_files(project_id=1)
downloaded = kb.project_files.download_project_file(pf_id) # base64 string
kb.project_files.remove_project_file(pf_id)
kb.project_files.remove_all_project_files(project_id=1)
# Task files — same pattern
tf_id = kb.task_files.create_task_file(
project_id=1,
task_id=42,
filename="screenshot.png",
blob=base64.b64encode(Path("screenshot.png").read_bytes()).decode(),
)
kb.task_files.remove_task_file(tf_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Project metadata
kb.project_metadata.save_project_metadata(project_id=1, values={"sprint": "Sprint 5"})
all_meta = kb.project_metadata.get_all_project_metadata(project_id=1)
sprint = kb.project_metadata.get_project_metadata_by_name(project_id=1, name="sprint")
kb.project_metadata.remove_project_metadata(project_id=1, name="sprint")
# Task metadata — same pattern
kb.task_metadata.save_task_metadata(task_id=42, values={"estimate": "8h"})
task_meta = kb.task_metadata.get_all_task_metadata(task_id=42)
kb.task_metadata.remove_task_metadata(task_id=42, name="estimate")with KanboardClient(url=URL, token=TOKEN) as kb:
# Users
kb.project_permissions.add_project_user(
project_id=1, user_id=5, role="project-member",
)
users = kb.project_permissions.get_project_users(project_id=1)
# users is dict[str, str]: {"5": "jdoe", ...}
role = kb.project_permissions.get_project_user_role(project_id=1, user_id=5)
kb.project_permissions.change_project_user_role(
project_id=1, user_id=5, role="project-manager",
)
kb.project_permissions.remove_project_user(project_id=1, user_id=5)
# Groups
kb.project_permissions.add_project_group(
project_id=1, group_id=2, role="project-viewer",
)
kb.project_permissions.change_project_group_role(
project_id=1, group_id=2, role="project-member",
)
kb.project_permissions.remove_project_group(project_id=1, group_id=2)
# Assignable users
assignable = kb.project_permissions.get_assignable_users(project_id=1)with KanboardClient(url=URL, token=TOKEN) as kb:
# Discover available actions and events
available = kb.actions.get_available_actions()
# {"\\TaskClose": "Close the task", "\\TaskAssignUser": "Assign user", ...}
events = kb.actions.get_available_action_events()
# {"task.move.column": "Move a task to another column", ...}
compatible = kb.actions.get_compatible_action_events("\\TaskClose")
# Create: close a task when moved to the last column
action_id = kb.actions.create_action(
project_id=1,
event_name="task.move.column",
action_name="\\TaskClose",
params={"column_id": "5"}, # param values must be strings
)
actions = kb.actions.get_actions(project_id=1)
kb.actions.remove_action(action_id)with KanboardClient(url=URL, token=TOKEN) as kb:
# Start a timer (user_id required when using user auth)
kb.subtask_time_tracking.set_subtask_start_time(subtask_id=10, user_id=1)
# Check status
running = kb.subtask_time_tracking.has_subtask_timer(subtask_id=10, user_id=1)
print("Timer running:", running) # True
# Stop
kb.subtask_time_tracking.set_subtask_end_time(subtask_id=10, user_id=1)
# Total hours spent
hours = kb.subtask_time_tracking.get_subtask_time_spent(subtask_id=10, user_id=1)
print(f"Time spent: {hours:.2f} hours")with KanboardClient(url=URL, token=TOKEN) as kb:
version = kb.application.get_version()
tz = kb.application.get_timezone()
colors = kb.application.get_default_task_colors()
default_color = kb.application.get_default_task_color()
app_roles = kb.application.get_application_roles()
project_roles = kb.application.get_project_roles()The me resource requires auth_mode="user" with a username and password.
from kanboard import KanboardClient
with KanboardClient(
url="https://kanboard.example.com/jsonrpc.php",
auth_mode="user",
username="admin",
password="your-password",
) as kb:
# Current user profile
me = kb.me.get_me()
print(me.username, me.email)
# Dashboard summary
dashboard = kb.me.get_my_dashboard()
# Activity stream
activity = kb.me.get_my_activity_stream()
# Projects the user is a member of
my_projects = kb.me.get_my_projects()
# Overdue tasks
overdue = kb.me.get_my_overdue_tasks()
# Create a private project
project_id = kb.me.create_my_private_project("My Private Project")Requires: kanboard-plugin-portfolio-management installed on your Kanboard server.
kb.portfolios exposes the 18 plugin JSON-RPC methods for server-side portfolio management (13 CRUD + 5 dependency queries). All methods return typed dataclass models (PluginPortfolio) or raw dicts for complex structures.
from kanboard import KanboardClient, PluginPortfolio
from kanboard.exceptions import KanboardNotFoundError, KanboardAPIError
with KanboardClient(url=URL, token=TOKEN) as kb:
# --- Portfolio CRUD ---
# Create (returns new portfolio ID)
portfolio_id = kb.portfolios.create_portfolio(
name="Platform Launch",
description="Q3 release programme",
owner_id=1,
)
# Read
pf = kb.portfolios.get_portfolio(portfolio_id) # -> PluginPortfolio
pf_by_name = kb.portfolios.get_portfolio_by_name("Platform Launch")
all_pfs = kb.portfolios.get_all_portfolios() # -> list[PluginPortfolio]
# Update
kb.portfolios.update_portfolio(portfolio_id, description="Updated description")
# Project membership
kb.portfolios.add_project_to_portfolio(portfolio_id, project_id=1)
kb.portfolios.add_project_to_portfolio(portfolio_id, project_id=2)
projects = kb.portfolios.get_portfolio_projects(portfolio_id) # list[dict]
memberships = kb.portfolios.get_project_portfolios(project_id=1) # list[PluginPortfolio]
kb.portfolios.remove_project_from_portfolio(portfolio_id, project_id=2)
# Task aggregation
tasks = kb.portfolios.get_portfolio_tasks(portfolio_id, status_id=1) # list[dict]
count = kb.portfolios.get_portfolio_task_count(portfolio_id) # dict
overview = kb.portfolios.get_portfolio_overview(portfolio_id) # dict
# --- Dependency queries (server-side SQL) ---
deps = kb.portfolios.get_portfolio_dependencies(portfolio_id)
deps_xp = kb.portfolios.get_portfolio_dependencies(portfolio_id, cross_project_only=True)
blocked = kb.portfolios.get_blocked_tasks(portfolio_id) # list[dict]
blocking = kb.portfolios.get_blocking_tasks(portfolio_id) # list[dict]
crit = kb.portfolios.get_portfolio_critical_path(portfolio_id) # list[dict]
graph = kb.portfolios.get_portfolio_dependency_graph(portfolio_id) # dict
# --- Cleanup ---
kb.portfolios.remove_portfolio(portfolio_id)Return conventions:
| Method | Returns | On error |
|---|---|---|
create_portfolio |
int (portfolio ID) |
KanboardAPIError on False |
get_portfolio |
PluginPortfolio |
KanboardNotFoundError on None |
get_portfolio_by_name |
PluginPortfolio |
KanboardNotFoundError on None or False |
get_all_portfolios |
list[PluginPortfolio] |
[] on False/None |
update_portfolio |
bool |
KanboardAPIError on False |
get_portfolio_projects |
list[dict] |
[] on False/None |
get_portfolio_tasks |
list[dict] |
[] on False/None |
get_portfolio_task_count |
dict |
{} on False/None |
get_portfolio_overview |
dict |
{} on False/None |
get_portfolio_dependency_graph |
dict |
{} on False/None |
| Dependency list methods | list[dict] |
[] on False/None |
Requires: kanboard-plugin-portfolio-management installed on your Kanboard server.
kb.milestones exposes the 10 plugin JSON-RPC methods for server-side milestone management. Progress is computed server-side (unlike the client-side PortfolioManager.get_milestone_progress()).
from kanboard import KanboardClient, PluginMilestone, PluginMilestoneProgress
from kanboard.exceptions import KanboardNotFoundError, KanboardAPIError
with KanboardClient(url=URL, token=TOKEN) as kb:
# Assume portfolio_id is already known (from kb.portfolios.create_portfolio or get_portfolio)
portfolio_id = 1
# Create milestone
milestone_id = kb.milestones.create_milestone(
portfolio_id=portfolio_id,
name="Beta Release",
target_date="2026-06-30",
description="First feature-complete release",
)
# Read
ms = kb.milestones.get_milestone(milestone_id) # -> PluginMilestone
all_ms = kb.milestones.get_portfolio_milestones(portfolio_id) # -> list[PluginMilestone]
# Update
kb.milestones.update_milestone(milestone_id, target_date="2026-07-15")
# Task membership
kb.milestones.add_task_to_milestone(milestone_id, task_id=42)
kb.milestones.add_task_to_milestone(milestone_id, task_id=99)
ms_tasks = kb.milestones.get_milestone_tasks(milestone_id) # -> list[dict]
task_ms = kb.milestones.get_task_milestones(task_id=42) # -> list[PluginMilestone]
kb.milestones.remove_task_from_milestone(milestone_id, task_id=42)
# Server-computed progress
progress = kb.milestones.get_milestone_progress(milestone_id) # -> PluginMilestoneProgress
print(f"{progress.percent:.0f}% complete")
print(f"At risk: {progress.is_at_risk}, Overdue: {progress.is_overdue}")
# Cleanup
kb.milestones.remove_milestone(milestone_id)Return conventions:
| Method | Returns | On error |
|---|---|---|
create_milestone |
int (milestone ID) |
KanboardAPIError on False |
get_milestone |
PluginMilestone |
KanboardNotFoundError on None |
get_portfolio_milestones |
list[PluginMilestone] |
[] on False/None |
update_milestone |
bool |
KanboardAPIError on False |
add_task_to_milestone |
bool |
KanboardAPIError on False |
get_milestone_tasks |
list[dict] |
[] on False/None |
get_task_milestones |
list[PluginMilestone] |
[] on False/None |
get_milestone_progress |
PluginMilestoneProgress |
KanboardNotFoundError on None |
The SDK provides a structured exception hierarchy. All exceptions inherit
from KanboardError, so you can catch them all with a single clause or
handle fine-grained sub-classes as needed.
KanboardError (base)
├── KanboardConfigError # Missing/invalid configuration
├── KanboardConnectionError # Network/connection failures
├── KanboardAuthError # HTTP 401/403, wrong credentials or auth mode
├── KanboardAPIError # JSON-RPC error responses
│ ├── KanboardNotFoundError # Resource not found (API returned null)
│ └── KanboardValidationError # Invalid parameters rejected by server
└── KanboardResponseError # Malformed/unparseable server response
from kanboard import KanboardClient
from kanboard.exceptions import KanboardError
with KanboardClient(url=URL, token=TOKEN) as kb:
try:
task = kb.tasks.get_task(999)
except KanboardError as exc:
print(f"Kanboard error: {exc}")from kanboard.exceptions import (
KanboardNotFoundError,
KanboardAuthError,
KanboardConnectionError,
KanboardAPIError,
)
with KanboardClient(url=URL, token=TOKEN) as kb:
try:
task = kb.tasks.get_task(task_id)
except KanboardNotFoundError:
print(f"Task {task_id} does not exist")
except KanboardAuthError as exc:
print(f"Authentication failed (HTTP {exc.http_status}): {exc.message}")
except KanboardConnectionError as exc:
print(f"Cannot reach {exc.url}: {exc.cause}")
except KanboardAPIError as exc:
print(f"API error (code={exc.code}, method={exc.method}): {exc.message}")When you call a me.* method with Application API auth, the SDK raises
KanboardAuthError with a clear message:
from kanboard.exceptions import KanboardAuthError
with KanboardClient(url=URL, token=TOKEN) as kb: # auth_mode="app" by default
try:
me = kb.me.get_me()
except KanboardAuthError as exc:
print(exc)
# → Authentication error: me.get_me() requires user auth mode ...| Exception | Extra Attributes |
|---|---|
KanboardConfigError |
field: str | None |
KanboardConnectionError |
url: str | None, cause: BaseException | None |
KanboardAuthError |
http_status: int | None |
KanboardAPIError |
method: str | None, code: int | None |
KanboardNotFoundError |
resource: str | None, identifier: str | int | None |
KanboardResponseError |
raw_body: str | bytes | None |
Send multiple JSON-RPC calls in a single HTTP request for efficiency. Responses are reordered to match the original call sequence.
from kanboard import KanboardClient
with KanboardClient(url=URL, token=TOKEN) as kb:
results = kb.batch([
("getVersion", {}),
("getTimezone", {}),
("getAllProjects", {}),
])
version, timezone, projects_raw = results
print(f"Server: {version} ({timezone})")
print(f"Projects returned: {len(projects_raw) if projects_raw else 0}")The batch() method raises KanboardAPIError if any individual call in the
batch returns an error. Use individual try/except blocks around the result
processing if partial failures are expected.
from kanboard.exceptions import KanboardAPIError
with KanboardClient(url=URL, token=TOKEN) as kb:
try:
results = kb.batch([
("getTask", {"task_id": 42}),
("getTask", {"task_id": 43}),
])
task_a_raw, task_b_raw = results
except KanboardAPIError as exc:
print(f"Batch failed: {exc}")For any Kanboard API method not yet wrapped in a resource class, use
client.call() directly:
from kanboard import KanboardClient
with KanboardClient(url=URL, token=TOKEN) as kb:
# Call any JSON-RPC method by name
result = kb.call("getVersion")
print(result) # "1.2.38"
# Pass keyword arguments as JSON-RPC params
task_raw = kb.call("getTask", task_id=42)
print(task_raw) # dict with raw task fieldscall() returns the raw Python object from the JSON-RPC result field
(dict, list, string, int, bool, or None). No dataclass conversion is
applied.
All SDK read methods return typed dataclass instances from kanboard.models.
Models are frozen (frozen=True) so they are hashable and immutable.
Common model fields:
| Model | Key Fields |
|---|---|
Task |
id, title, project_id, column_id, is_active, priority, color_id, date_due, owner_id |
Project |
id, name, is_active, is_public, owner_id, identifier, last_modified |
Column |
id, title, position, task_limit, project_id |
Swimlane |
id, name, project_id, position, is_active |
Comment |
id, task_id, user_id, username, comment, date_creation |
Subtask |
id, title, task_id, user_id, status, time_estimated, time_spent |
User |
id, username, name, email, role, is_active |
Tag |
id, name, project_id, color_id |
Group |
id, name, external_id |
Models support dataclasses.asdict() for serialization:
import dataclasses
from kanboard import KanboardClient
with KanboardClient(url=URL, token=TOKEN) as kb:
task = kb.tasks.get_task(42)
task_dict = dataclasses.asdict(task)
print(task_dict["title"])The kanboard.orchestration subpackage provides portfolio management, cross-project milestones, dependency analysis, and critical-path computation. It supports two interchangeable backends:
| Backend | Class | Description |
|---|---|---|
| Local JSON | LocalPortfolioStore |
Persists portfolios to ~/.config/kanboard/portfolios.json. Works with any standard Kanboard instance — no plugin required. |
| Remote plugin | RemotePortfolioBackend |
Delegates to the Kanboard Portfolio plugin via JSON-RPC. Requires the plugin installed on your server. |
The orchestration classes are opt-in and not wired into KanboardClient. Callers instantiate them separately, passing a KanboardClient as a constructor argument.
from kanboard import KanboardClient
from kanboard.orchestration import (
DependencyAnalyzer,
LocalPortfolioStore,
PortfolioManager,
RemotePortfolioBackend,
create_backend,
)
# All exports are also available from the top-level package:
from kanboard import (
DependencyAnalyzer,
LocalPortfolioStore,
PortfolioManager,
RemotePortfolioBackend,
create_backend,
)Use the create_backend() factory to select the backend at runtime, driven by configuration:
from kanboard import KanboardClient, create_backend
from kanboard.orchestration import PortfolioManager
with KanboardClient(url=URL, token=TOKEN) as kb:
# Local backend (no plugin required)
local_backend = create_backend("local")
# Remote backend (plugin required)
remote_backend = create_backend("remote", client=kb)
# PortfolioManager works transparently with either backend
manager = PortfolioManager(kb, local_backend)
tasks = manager.get_portfolio_tasks("Platform Launch")
# Switch to remote — identical API
remote_manager = PortfolioManager(kb, remote_backend)
tasks = remote_manager.get_portfolio_tasks("Platform Launch")Driving backend selection from KanboardConfig:
from kanboard import KanboardClient
from kanboard.config import KanboardConfig
from kanboard.orchestration import PortfolioManager, create_backend
config = KanboardConfig.resolve()
# config.portfolio_backend is "local" or "remote"
with KanboardClient(url=config.url, token=config.token) as kb:
backend = create_backend(config.portfolio_backend, client=kb)
manager = PortfolioManager(kb, backend)The PortfolioBackend protocol defines the interface that both LocalPortfolioStore and RemotePortfolioBackend satisfy:
from kanboard.orchestration import PortfolioBackend
# Type-check backend conformance at runtime
from kanboard import LocalPortfolioStore, RemotePortfolioBackend
assert isinstance(LocalPortfolioStore(), PortfolioBackend) # TrueLocalPortfolioStore provides JSON-backed CRUD for portfolios and milestones. By default it persists to ~/.config/kanboard/portfolios.json.
from pathlib import Path
from kanboard.orchestration import LocalPortfolioStore
# Default path: ~/.config/kanboard/portfolios.json
store = LocalPortfolioStore()
# Custom path (useful for testing)
store = LocalPortfolioStore(path=Path("/tmp/my-portfolios.json"))# Create
store.create_portfolio(
name="Platform Launch",
description="Q3 release programme",
project_ids=[1, 2, 3],
)
# Read
portfolio = store.get_portfolio("Platform Launch") # raises KanboardConfigError if not found
all_portfolios = store.load() # returns list[Portfolio]
# Update fields
store.update_portfolio("Platform Launch", description="Q3+Q4 release")
# Project membership
store.add_project("Platform Launch", project_id=4)
store.remove_project("Platform Launch", project_id=1)
# Delete
removed = store.remove_portfolio("Platform Launch") # returns True/Falsefrom datetime import datetime
# Add milestone to a portfolio
store.add_milestone(
portfolio_name="Platform Launch",
milestone_name="Beta Release",
target_date=datetime(2026, 6, 30),
)
# Update milestone fields
store.update_milestone(
"Platform Launch", "Beta Release",
target_date=datetime(2026, 7, 15),
)
# Task membership
store.add_task_to_milestone(
portfolio_name="Platform Launch",
milestone_name="Beta Release",
task_id=42,
critical=True, # adds to both task_ids and critical_task_ids
)
store.remove_task_from_milestone("Platform Launch", "Beta Release", task_id=42)
# Delete milestone
store.remove_milestone("Platform Launch", "Beta Release")PortfolioManager aggregates task data across multiple projects and computes milestone progress. It makes N+1 API calls by design (one per project/task) — acceptable for Phase 0; the Kanboard Portfolio plugin will solve this at scale in Phase 1.
from kanboard import KanboardClient
from kanboard.orchestration import LocalPortfolioStore, PortfolioManager
with KanboardClient(url=URL, token=TOKEN) as kb:
store = LocalPortfolioStore()
manager = PortfolioManager(kb, store)
# Fetch all projects in the portfolio (skips deleted projects with a warning)
projects = manager.get_portfolio_projects("Platform Launch")
# Aggregate tasks across all portfolio projects
tasks = manager.get_portfolio_tasks("Platform Launch")
# Filter by status (1=active, 0=closed), assignee, or specific project
active_tasks = manager.get_portfolio_tasks(
"Platform Launch",
status=1,
assignee_id=7,
project_id=2,
)
# Milestone progress
progress = manager.get_milestone_progress("Platform Launch", "Beta Release")
print(f"{progress.milestone_name}: {progress.percent:.0f}%")
print(f"At risk: {progress.is_at_risk}, Overdue: {progress.is_overdue}")
print(f"Blocked tasks: {progress.blocked_task_ids}")
# All milestones at once
all_progress = manager.get_all_milestone_progress("Platform Launch")
for p in all_progress:
status = "🔴 OVERDUE" if p.is_overdue else ("⚠ AT RISK" if p.is_at_risk else "✓")
print(f" {status} {p.milestone_name}: {p.percent:.0f}%")
# Sync portfolio/milestone membership to Kanboard metadata
result = manager.sync_metadata("Platform Launch")
print(f"Synced {result['projects_synced']} projects, {result['tasks_synced']} tasks")Milestone progress thresholds:
| Condition | Logic |
|---|---|
is_at_risk |
target_date within 7 days and completion < 80% |
is_overdue |
target_date in the past and completion < 100% |
blocked_task_ids |
Tasks with at least one unresolved is blocked by link to an open task |
DependencyAnalyzer builds directed dependency graphs from Kanboard task links (blocks/is blocked by). It uses topological sort (Kahn's algorithm) for critical-path computation and deduplicates bidirectional edges.
from kanboard import KanboardClient
from kanboard.orchestration import DependencyAnalyzer
with KanboardClient(url=URL, token=TOKEN) as kb:
analyzer = DependencyAnalyzer(kb)
# Fetch tasks first (e.g. from PortfolioManager or a single project)
tasks = kb.tasks.get_all_tasks(project_id=1, status_id=1)
# Get all dependency edges
edges = analyzer.get_dependency_edges(tasks)
# Cross-project edges only
xp_edges = analyzer.get_dependency_edges(tasks, cross_project_only=True)
# Tasks with unresolved blockers
blocked = analyzer.get_blocked_tasks(tasks)
for task, blocking_edges in blocked:
print(f"#{task.id} {task.title} is blocked by:")
for edge in blocking_edges:
print(f" #{edge.opposite_task_id} {edge.opposite_task_title}")
# Open tasks that are blocking others
blocking = analyzer.get_blocking_tasks(tasks)
# Critical path (longest dependency chain)
critical = analyzer.get_critical_path(tasks)
for i, task in enumerate(critical, start=1):
print(f" {i}. #{task.id} {task.title}")
# Full dependency graph (for custom rendering or export)
graph = analyzer.get_dependency_graph(tasks, cross_project_only=False)
# graph = {"nodes": [{"id": ..., "title": ..., ...}], "edges": [...]}Edge deduplication: Kanboard returns links from both sides of a relationship. DependencyAnalyzer normalises all edges to the canonical (blocker_id, blocked_id) direction and deduplicates, so processing both Task A and Task B produces exactly one edge for each A blocks B relationship.
Cycle detection: If a dependency cycle is detected, a warning is logged and a partial result is returned. Cycles should not occur with standard blocks/is blocked by usage.
The four orchestration models are defined in kanboard.models and exported from the top-level kanboard package. Unlike resource models, they have no from_api() classmethod — they are composed client-side from multiple API responses.
from kanboard import Portfolio, Milestone, MilestoneProgress, DependencyEdge| Model | Key Fields |
|---|---|
Portfolio |
name, description, project_ids: list[int], milestones: list[Milestone], created_at, updated_at |
Milestone |
name, portfolio_name, target_date, task_ids: list[int], critical_task_ids: list[int] |
MilestoneProgress |
milestone_name, portfolio_name, target_date, total, completed, percent: float, is_at_risk, is_overdue, blocked_task_ids |
DependencyEdge |
task_id, task_title, task_project_id, task_project_name, opposite_task_id, opposite_task_title, opposite_task_project_id, opposite_task_project_name, link_label, is_cross_project, is_resolved |
All four models are mutable (no frozen=True) to support in-place editing before saving to the store.
When using the remote backend or calling kb.portfolios / kb.milestones directly, the API returns three additional typed dataclass models. Unlike the orchestration models above, these do have from_api() classmethods (they are server-side entities, not client-side constructs).
from kanboard import PluginPortfolio, PluginMilestone, PluginMilestoneProgress| Model | Key Fields |
|---|---|
PluginPortfolio |
id, name, description, owner_id, is_active, created_at, updated_at |
PluginMilestone |
id, portfolio_id, name, description, target_date, status, color_id, owner_id, created_at, updated_at |
PluginMilestoneProgress |
milestone_id, total, completed, percent (float), is_at_risk, is_overdue |
These models are returned by PortfoliosResource and MilestonesResource methods. The RemotePortfolioBackend internally converts PluginPortfolio + PluginMilestone into the orchestration-layer Portfolio and Milestone models so that PortfolioManager can work with either backend transparently.