Interact with Kanboard project management via JSON-RPC API. Use when working with Kanboard tasks, projects, boards, columns, swimlanes, comments, subtasks, a...
---
name: kanboard
version: 1.0.0
description: >
Interact with Kanboard project management via JSON-RPC API.
Use when working with Kanboard tasks, projects, boards, columns, swimlanes,
comments, subtasks, and users. Trigger on: "create task", "move task",
"show board", "list projects", "add comment", "kanboard", "kanban task".
Supports both Application API (token) and User API (login/password).
metadata:
clawdbot:
env:
- KANBOARD_URL # e.g. https://kanboard.example.com
- KANBOARD_API_TOKEN # from Settings → API, used as password with user "jsonrpc"
# Optional: use personal credentials instead of app token
# - KANBOARD_USER # your Kanboard username
# - KANBOARD_PASS # your Kanboard password or personal API token
requires:
- curl
- jq
---
# Kanboard Skill
## Overview
Kanboard uses **JSON-RPC 2.0** over HTTP POST. All calls go to a single endpoint.
### Auth modes
| Mode | User | Password |
|------|------|----------|
| Application API | `jsonrpc` | `$KANBOARD_API_TOKEN` |
| User API | `$KANBOARD_USER` | `$KANBOARD_PASS` |
Application API skips permission checks and has no session. Use it for automation.
User API respects project permissions; required for "My…" procedures.
---
## Core Helper
Always use this shell function to call the API:
```bash
kb() {
local method="$1"
local params="${2:-{}}"
local user="${KANBOARD_USER:-jsonrpc}"
local pass="${KANBOARD_PASS:-$KANBOARD_API_TOKEN}"
curl -s -X POST \
-u "$user:$pass" \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"id\":1,\"params\":$params}" \
"${KANBOARD_URL}/jsonrpc.php" | jq .
}
```
Check for errors in every response:
```bash
# Always verify result is not null/false
result=$(kb getMe | jq '.result')
if [ "$result" = "null" ] || [ "$result" = "false" ]; then
echo "Error: $(kb getMe | jq -r '.error.message // "unknown error"')"
fi
```
---
## Projects
```bash
# List all projects
kb getAllProjects
# Get single project by ID
kb getProjectById '{"project_id": 1}'
# Get project by name
kb getProjectByName '{"name": "My Project"}'
# Create project
kb createProject '{"name": "New Project", "description": "Optional description"}'
# Update project
kb updateProject '{"id": 1, "name": "Renamed", "description": "Updated"}'
# Remove project (irreversible)
kb removeProject '{"project_id": 1}'
# Enable / disable project
kb enableProject '{"project_id": 1}'
kb disableProject '{"project_id": 1}'
# Get project activity feed
kb getProjectActivity '{"project_id": 1}'
```
---
## Board & Columns
```bash
# Get full board (columns + tasks) for a project
kb getBoard '{"project_id": 1}'
# List columns
kb getColumns '{"project_id": 1}'
# Get single column
kb getColumn '{"column_id": 5}'
# Create column
kb addColumn '{"project_id": 1, "title": "In Review", "task_limit": 3}'
# Update column
kb updateColumn '{"column_id": 5, "title": "Review", "task_limit": 5}'
# Remove column
kb removeColumn '{"column_id": 5}'
# Change column position
kb changeColumnPosition '{"project_id": 1, "column_id": 5, "position": 2}'
```
---
## Tasks
```bash
# Create task (minimum required: title + project_id)
kb createTask '{
"title": "Fix login bug",
"project_id": 1,
"column_id": 2,
"swimlane_id": 1,
"color_id": "red",
"priority": 2,
"due_date": "2025-12-31",
"description": "Detailed description here",
"owner_id": 3,
"tags": ["bug", "urgent"]
}'
# Get task by ID
kb getTask '{"task_id": 42}'
# Get task by reference (external ref)
kb getTaskByReference '{"project_id": 1, "reference": "EXT-123"}'
# List all tasks in a project (status: 1=open, 2=closed)
kb getAllTasks '{"project_id": 1, "status_id": 1}'
# Search tasks with advanced query
kb searchTasks '{"project_id": 1, "query": "assignee:me status:open"}'
# Update task
kb updateTask '{
"id": 42,
"title": "Fix login bug (updated)",
"column_id": 3,
"color_id": "green",
"priority": 1,
"due_date": "2025-11-30"
}'
# Move task to another column/swimlane/position
kb moveTaskToColumn '{
"project_id": 1,
"task_id": 42,
"column_id": 3,
"position": 1,
"swimlane_id": 1
}'
# Move task to another project
kb moveTaskToProject '{
"task_id": 42,
"project_id": 2,
"swimlane_id": 1,
"column_id": 1,
"category_id": 0
}'
# Duplicate task to another project
kb duplicateTaskToProject '{
"task_id": 42,
"project_id": 2
}'
# Close / Open task
kb closeTask '{"task_id": 42}'
kb openTask '{"task_id": 42}'
# Remove task (irreversible)
kb removeTask '{"task_id": 42}'
# Get task color list
kb getTaskColors
```
### Task color IDs
`yellow`, `blue`, `green`, `purple`, `red`, `orange`, `grey`, `brown`, `deep_orange`, `dark_grey`, `pink`, `teal`, `cyan`, `lime`, `light_green`, `amber`
---
## Subtasks
```bash
# List subtasks for a task
kb getAllSubtasks '{"task_id": 42}'
# Create subtask
kb createSubtask '{
"task_id": 42,
"title": "Write unit tests",
"user_id": 3,
"time_estimated": 4
}'
# Update subtask (status: 0=todo, 1=in-progress, 2=done)
kb updateSubtask '{
"id": 10,
"task_id": 42,
"status": 1,
"time_spent": 2
}'
# Remove subtask
kb removeSubtask '{"subtask_id": 10}'
```
---
## Comments
```bash
# List comments for a task
kb getAllComments '{"task_id": 42}'
# Create comment
kb createComment '{
"task_id": 42,
"user_id": 1,
"content": "This is a **markdown** comment."
}'
# Update comment
kb updateComment '{"id": 7, "content": "Updated comment text."}'
# Remove comment
kb removeComment '{"comment_id": 7}'
```
---
## Swimlanes
```bash
# List swimlanes for a project
kb getSwimlanes '{"project_id": 1}'
# Get active swimlanes only
kb getActiveSwimlanes '{"project_id": 1}'
# Create swimlane
kb addSwimlane '{"project_id": 1, "name": "Team Alpha"}'
# Update swimlane
kb updateSwimlane '{"swimlane_id": 3, "name": "Team Beta"}'
# Remove swimlane
kb removeSwimlane '{"project_id": 1, "swimlane_id": 3}'
# Change swimlane position
kb changeSwimlanePosition '{"project_id": 1, "swimlane_id": 3, "position": 1}'
```
---
## Categories
```bash
# List categories for a project
kb getAllCategories '{"project_id": 1}'
# Create category
kb createCategory '{"project_id": 1, "name": "Backend"}'
# Update category
kb updateCategory '{"id": 5, "name": "Backend & API"}'
# Remove category
kb removeCategory '{"category_id": 5}'
```
---
## Users
```bash
# List all users (Application API only)
kb getAllUsers
# Get user by ID
kb getUserById '{"user_id": 3}'
# Get user by username
kb getUserByName '{"username": "alice"}'
# Create user
kb createUser '{
"username": "bob",
"password": "S3cur3P@ss",
"name": "Bob Smith",
"email": "bob@example.com",
"role": "app-user"
}'
# Roles: app-admin | app-manager | app-user
# Update user
kb updateUser '{"id": 3, "name": "Bob Jones", "email": "bob.jones@example.com"}'
# Disable / Enable user
kb disableUser '{"user_id": 3}'
kb enableUser '{"user_id": 3}'
# Remove user
kb removeUser '{"user_id": 3}'
# Current user (User API only)
kb getMe
kb getMyProjects
kb getMyDashboard
kb getMyActivityStream
kb getMyCalendar
kb getMyNotifications
```
---
## Project Permissions
```bash
# List project users
kb getProjectUsers '{"project_id": 1}'
# Add user to project
kb addProjectUser '{
"project_id": 1,
"user_id": 3,
"role": "project-member"
}'
# Roles: project-manager | project-member | project-viewer
# Change user role in project
kb changeProjectUserRole '{"project_id": 1, "user_id": 3, "role": "project-manager"}'
# Remove user from project
kb removeProjectUser '{"project_id": 1, "user_id": 3}'
# Add/remove group to project
kb addProjectGroup '{"project_id": 1, "group_id": 2, "role": "project-member"}'
kb removeProjectGroup '{"project_id": 1, "group_id": 2}'
```
---
## Tags
```bash
# Get all tags for a project
kb getTagsByProject '{"project_id": 1}'
# Create tag
kb createTag '{"project_id": 1, "tag": "urgent"}'
# Update tag
kb updateTag '{"id": 4, "tag": "critical"}'
# Remove tag
kb removeTag '{"tag_id": 4}'
# Get tags for a task
kb getTaskTags '{"task_id": 42}'
# Assign tags to a task (replaces existing tags)
kb setTaskTags '{"project_id": 1, "task_id": 42, "tags": ["bug", "urgent"]}'
```
---
## Task Links (Internal)
```bash
# Get link types
kb getAllLinks
# Get links for a task
kb getAllTaskLinks '{"task_id": 42}'
# Create task link
kb createTaskLink '{
"task_id": 42,
"opposite_task_id": 55,
"link_id": 1
}'
# Common link_id: 1=relates to, 2=blocks, 3=is blocked by, 4=duplicates, 5=is duplicated by
# Remove task link
kb removeTaskLink '{"task_link_id": 8}'
```
---
## Application
```bash
# Get app version
kb getVersion
# Get app timezone
kb getTimezone
# Get app default language
kb getDefaultLanguage
# Get current datetime
kb now
# Get available board column types
kb getDefaultTaskColors
```
---
## Common Workflows
### Create project with full setup
```bash
# 1. Create project
project_id=$(kb createProject '{"name":"Sprint 1"}' | jq '.result')
# 2. Add columns
kb addColumn "{\"project_id\": $project_id, \"title\": \"Backlog\"}"
kb addColumn "{\"project_id\": $project_id, \"title\": \"In Progress\", \"task_limit\": 3}"
kb addColumn "{\"project_id\": $project_id, \"title\": \"Review\"}"
kb addColumn "{\"project_id\": $project_id, \"title\": \"Done\"}"
# 3. Add swimlane
kb addSwimlane "{\"project_id\": $project_id, \"name\": \"Team Alpha\"}"
# 4. Show board
kb getBoard "{\"project_id\": $project_id}"
```
### Move task through workflow
```bash
task_id=42
project_id=1
# Get column IDs first
columns=$(kb getColumns "{\"project_id\": $project_id}" | jq '.result')
in_progress_col=$(echo $columns | jq '[.[] | select(.title=="In Progress")][0].id')
# Move task
kb moveTaskToColumn "{
\"project_id\": $project_id,
\"task_id\": $task_id,
\"column_id\": $in_progress_col,
\"position\": 1
}"
```
### Create task with subtasks
```bash
# Create parent task
task_id=$(kb createTask '{
"title": "Implement feature X",
"project_id": 1,
"priority": 2
}' | jq '.result')
# Add subtasks
kb createSubtask "{\"task_id\": $task_id, \"title\": \"Write spec\"}"
kb createSubtask "{\"task_id\": $task_id, \"title\": \"Implement\"}"
kb createSubtask "{\"task_id\": $task_id, \"title\": \"Write tests\"}"
kb createSubtask "{\"task_id\": $task_id, \"title\": \"Code review\"}"
```
### Bulk close completed tasks
```bash
project_id=1
# Get all open tasks, close those tagged "done"
kb getAllTasks "{\"project_id\": $project_id, \"status_id\": 1}" \
| jq -r '.result[] | select(.tags[]? == "done") | .id' \
| while read task_id; do
kb closeTask "{\"task_id\": $task_id}"
echo "Closed task $task_id"
done
```
---
## Error Handling
```bash
# Robust call wrapper
kb_safe() {
local result
result=$(kb "$@")
local error=$(echo "$result" | jq -r '.error // empty')
if [ -n "$error" ]; then
echo "❌ API Error: $(echo "$result" | jq -r '.error.message')" >&2
return 1
fi
echo "$result" | jq '.result'
}
# Usage
kb_safe getAllProjects
kb_safe getTask '{"task_id": 99999}' # returns error if not found
```
---
## Setup & Configuration
Add to your OpenClaw environment:
```bash
# Required
export KANBOARD_URL="https://kanboard.example.com"
export KANBOARD_API_TOKEN="your_token_from_settings_page"
# Optional (for User API / "My…" procedures)
export KANBOARD_USER="your_username"
export KANBOARD_PASS="your_password_or_personal_token"
```
**Getting your API token:**
1. Log in to Kanboard as admin
2. Go to **Settings → API**
3. Copy the token shown there
**Personal API token (User API):**
1. Click your profile avatar → **My Profile**
2. Click **"Generate a new API token"** in the API section
3. Use as `KANBOARD_PASS` with your username as `KANBOARD_USER`
---
## Notes
- All dates use `YYYY-MM-DD` format or Unix timestamps
- Task `priority`: 0=low, 1=normal, 2=high, 3=urgent
- Kanboard supports **batch requests** — multiple JSON-RPC calls in one HTTP request (useful for bulk ops)
- `status_id` for tasks: 1=open, 2=closed
- API endpoint is always `<KANBOARD_URL>/jsonrpc.php`don't have the plugin yet? install it then click "run inline in claude" again.
this skill lets you interact with kanboard project management via its json-rpc 2.0 api. use it when you need to create, move, or manage tasks, projects, boards, columns, swimlanes, comments, subtasks, or users. works with both application api tokens (for automation, no permission checks) and user api credentials (for permission-respecting operations like "my projects" or "my activity"). trigger phrases: "create task", "move task", "show board", "list projects", "add comment", "kanboard", "kanban task".
environment variables (required):
KANBOARD_URL: base url of kanboard instance, e.g. https://kanboard.example.comKANBOARD_API_TOKEN: application api token from Settings > API, used as password with user jsonrpcenvironment variables (optional, for user api):
KANBOARD_USER: your kanboard username (enables user api mode with permission checks)KANBOARD_PASS: your kanboard password or personal api token (required if KANBOARD_USER is set)external dependencies:
curl: http client for json-rpc requestsjq: json processor for parsing responsesauthentication modes:
| mode | user | password | use case |
|---|---|---|---|
| application api | jsonrpc |
$KANBOARD_API_TOKEN |
automation, admin operations, no permission checks |
| user api | $KANBOARD_USER |
$KANBOARD_PASS |
permission-respecting ops, "my" procedures, session-aware |
note: if both KANBOARD_USER and KANBOARD_PASS are set, user api takes precedence. application api ignores kanboard permission model.
core helper function (use for all api calls):
kb() {
local method="$1"
local params="${2:-{}}"
local user="${KANBOARD_USER:-jsonrpc}"
local pass="${KANBOARD_PASS:-$KANBOARD_API_TOKEN}"
curl -s -X POST \
-u "$user:$pass" \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"id\":1,\"params\":$params}" \
"${KANBOARD_URL}/jsonrpc.php" | jq .
}
error checking on all responses:
result=$(kb getMe | jq '.result')
if [ "$result" = "null" ] || [ "$result" = "false" ]; then
error_msg=$(kb getMe | jq -r '.error.message // "unknown error"')
echo "error: $error_msg" >&2
return 1
fi
step 1: list all projects
input: none (method call only)
command: kb getAllProjects
output: json array of project objects with keys: id, name, description, is_active, is_public, etc.
edge case: returns empty array if no projects exist or if user lacks permission to view any
step 2: get single project by id
input: project_id (integer)
command: kb getProjectById '{"project_id": 1}'
output: single project object
edge case: returns null if project does not exist or user lacks permission
step 3: get project by name
input: name (string, case-sensitive)
command: kb getProjectByName '{"name": "My Project"}'
output: single project object or null if not found
note: exact name match required
step 4: create project
input: name (required, string), description (optional, string)
command: kb createProject '{"name": "New Project", "description": "Optional"}'
output: integer project id
edge case: fails if project name already exists; returns error with code -32602
step 5: update project
input: id (integer), name (string), description (string)
command: kb updateProject '{"id": 1, "name": "Renamed", "description": "Updated"}'
output: boolean true on success
edge case: fails silently if project id does not exist
step 6: remove project
input: project_id (integer)
command: kb removeProject '{"project_id": 1}'
output: boolean true on success
edge case: irreversible; cascades to all tasks, swimlanes, columns in that project
step 7: enable/disable project
input: project_id (integer)
commands: kb enableProject '{"project_id": 1}' or kb disableProject '{"project_id": 1}'
output: boolean true on success
edge case: disabled projects still exist but do not appear in user dashboards
step 8: get project activity feed
input: project_id (integer)
command: kb getProjectActivity '{"project_id": 1}'
output: json array of activity log entries with timestamp, user, action, task
edge case: limited to recent entries (usually last 100); may be empty if no activity
step 9: get full board (columns and tasks)
input: project_id (integer)
command: kb getBoard '{"project_id": 1}'
output: json object with columns array, each containing tasks
note: preferred over separate column + task calls for performance
step 10: list columns for project
input: project_id (integer)
command: kb getColumns '{"project_id": 1}'
output: json array of column objects with keys: id, title, task_limit, position
edge case: empty array if no columns (unusual but possible in new projects)
step 11: get single column
input: column_id (integer)
command: kb getColumn '{"column_id": 5}'
output: single column object
edge case: returns null if column does not exist
step 12: create column
input: project_id (integer), title (string, required), task_limit (integer, optional, 0=unlimited)
command: kb addColumn '{"project_id": 1, "title": "In Review", "task_limit": 3}'
output: integer column id
edge case: column position auto-assigned to end of board
step 13: update column
input: column_id (integer), title (string), task_limit (integer)
command: kb updateColumn '{"column_id": 5, "title": "Review", "task_limit": 5}'
output: boolean true on success
step 14: remove column
input: column_id (integer)
command: kb removeColumn '{"column_id": 5}'
output: boolean true on success
edge case: irreversible; tasks in that column are deleted or moved to first column (depends on kanboard version)
step 15: change column position
input: project_id (integer), column_id (integer), position (integer, 1-based)
command: kb changeColumnPosition '{"project_id": 1, "column_id": 5, "position": 2}'
output: boolean true on success
edge case: position values auto-clamped to valid range
step 16: create task
input: title (required, string), project_id (required, integer), column_id (optional, integer, defaults to first column), swimlane_id (optional), color_id (optional, string from color list), priority (optional, 0-3), due_date (optional, yyyy-mm-dd or unix timestamp), description (optional, supports markdown), owner_id (optional, integer user id), tags (optional, array of strings)
command:
kb createTask '{
"title": "Fix login bug",
"project_id": 1,
"column_id": 2,
"swimlane_id": 1,
"color_id": "red",
"priority": 2,
"due_date": "2025-12-31",
"description": "Detailed description here",
"owner_id": 3,
"tags": ["bug", "urgent"]
}'
output: integer task id
edge case: if column_id or swimlane_id missing, kanboard assigns defaults
step 17: get task by id
input: task_id (integer)
command: kb getTask '{"task_id": 42}'
output: task object with all fields (title, description, assigned user, due date, subtasks count, comments count, etc.)
edge case: returns null if task does not exist
step 18: get task by external reference
input: project_id (integer), reference (string)
command: kb getTaskByReference '{"project_id": 1, "reference": "EXT-123"}'
output: task object or null
note: requires task to have external reference field set
step 19: list all tasks in project
input: project_id (integer), status_id (optional, 1=open, 2=closed, omit for all)
command: kb getAllTasks '{"project_id": 1, "status_id": 1}'
output: json array of task objects
edge case: large projects (1000+ tasks) may hit rate limits or timeout; consider pagination via swimlane or column
step 20: search tasks with query
input: project_id (integer), query (string, supports operators like assignee:me, status:open, color:red, priority:2, tag:urgent, etc.)
command: kb searchTasks '{"project_id": 1, "query": "assignee:me status:open"}'
output: json array of matching task objects
edge case: complex queries may be slow on large boards
step 21: update task
input: id (integer), plus any fields to update: title, column_id, color_id, priority, due_date, description, owner_id, swimlane_id
command:
kb updateTask '{
"id": 42,
"title": "Fix login bug (updated)",
"column_id": 3,
"color_id": "green",
"priority": 1,
"due_date": "2025-11-30"
}'
output: boolean true on success edge case: omitted fields are not changed; does not create partial null values
step 22: move task to another column
input: project_id (integer), task_id (integer), column_id (integer), position (optional, 1-based, defaults to end of column), swimlane_id (optional, defaults to default swimlane)
command:
kb moveTaskToColumn '{
"project_id": 1,
"task_id": 42,
"column_id": 3,
"position": 1,
"swimlane_id": 1
}'
output: boolean true on success edge case: moving task can trigger automation rules (e.g., auto-assignment)
step 23: move task to another project
input: task_id (integer), project_id (integer), swimlane_id (optional), column_id (optional), category_id (optional, 0=none)
command:
kb moveTaskToProject '{
"task_id": 42,
"project_id": 2,
"swimlane_id": 1,
"column_id": 1,
"category_id": 0
}'
output: boolean true on success edge case: task ownership and permissions may be reset if target project is different
step 24: duplicate task to another project
input: task_id (integer), project_id (integer)
command: kb duplicateTaskToProject '{"task_id": 42, "project_id": 2}'
output: integer id of new duplicated task
note: comments and subtasks are not duplicated
step 25: close task
input: task_id (integer)
command: kb closeTask '{"task_id": 42}'
output: boolean true on success
edge case: does not move task to any column; status changed to closed but position preserved
step 26: open task
input: task_id (integer)
command: kb openTask '{"task_id": 42}'
output: boolean true on success
step 27: remove task
input: task_id (integer)
command: kb removeTask '{"task_id": 42}'
output: boolean true on success
edge case: irreversible; also deletes all comments, subtasks, and links for that task
step 28: get task color list
input: none
command: kb getTaskColors
output: json object mapping color names to hex codes
valid color ids: yellow, blue, green, purple, red, orange, grey, brown, deep_orange, dark_grey, pink, teal, cyan, lime, light_green, amber
step 29: list subtasks for task
input: task_id (integer)
command: kb getAllSubtasks '{"task_id": 42}'
output: json array of subtask objects with id, title, status, user_id, time_estimated, time_spent
edge case: empty array if task has no subtasks
step 30: create subtask
input: task_id (integer), title (string), user_id (optional, integer), time_estimated (optional, hours as float)
command: kb createSubtask '{"task_id": 42, "title": "Write unit tests", "user_id": 3, "time_estimated": 4}'
output: integer subtask id
step 31: update subtask
input: id (integer), task_id (integer), status (0=todo, 1=in-progress, 2=done), time_spent (optional, hours as float)
command: kb updateSubtask '{"id": 10, "task_id": 42, "status": 1, "time_spent": 2}'
output: boolean true on success
note: status change to 2 (done) auto-increments time_spent if not provided
step 32: remove subtask
input: subtask_id (integer)
command: kb removeSubtask '{"subtask_id": 10}'
output: boolean true on success
edge case: irreversible
step 33: list comments for task
input: task_id (integer)
command: kb getAllComments '{"task_id": 42}'
output: json array of comment objects with id, content, user_id, username, date_creation, date_modification
edge case: empty array if no comments
step 34: create comment
input: task_id (integer), user_id (optional, defaults to current user in user api mode), content (string, supports markdown)
command: kb createComment '{"task_id": 42, "user_id": 1, "content": "This is a **markdown** comment."}'
output: integer comment id
edge case: in application api mode, user_id is required
step 35: update comment
input: id (integer), content (string)
command: kb updateComment '{"id": 7, "content": "Updated comment text."}'
output: boolean true on success
edge case: only comment creator or admin can update
step 36: remove comment
input: comment_id (integer)
command: kb removeComment '{"comment_id": 7}'
output: boolean true on success
edge case: irreversible
step 37: list swimlanes for project
input: project_id (integer)
command: kb getSwimlanes '{"project_id": 1}'
output: json array of swimlane objects with id, name, position, is_active
step 38: get active swimlanes only
input: project_id (integer)
command: kb getActiveSwimlanes '{"project_id": 1}'
output: json array of active swimlane objects (is_active = true)
step 39: create swimlane
input: project_id (integer), name (string)
command: kb addSwimlane '{"project_id": 1, "name": "Team Alpha"}'
output: integer swimlane id
step 40: update swimlane
input: swimlane_id (integer), name (string)
command: kb updateSwimlane '{"swimlane_id": 3, "name": "Team Beta"}'
output: boolean true on success
step 41: remove swimlane
input: project_id (integer), swimlane_id (integer)
command: kb removeSwimlane '{"project_id": 1, "swimlane_id": 3}'
output: boolean true on success
edge case: irreversible; tasks in swimlane moved to default swimlane or first swimlane
step 42: change swimlane position
input: project_id (integer), swimlane_id (integer), position (integer, 1-based)
command: kb changeSwimlanePosition '{"project_id": 1, "swimlane_id": 3, "position": 1}'
output: boolean true on success
step 43: list categories for project
input: project_id (integer)
command: kb getAllCategories '{"project_id": 1}'
output: json array of category objects with id, name, project_id
step 44: create category
input: `project_