diff --git a/plans/design-bot-users.md b/plans/design-bot-users.md
new file mode 100644
index 000000000..539d1cb0a
--- /dev/null
+++ b/plans/design-bot-users.md
@@ -0,0 +1,477 @@
+# Design: Bot Users
+
+## Overview
+
+Bot users are first-class users with an `is_bot` flag. They appear alongside human users in assignee pickers, comment threads, and everywhere else users show up. The only visual difference is a small "Bot" badge next to their name.
+
+Bot users are always owned by a human user who created them. They cannot log in via username/password — they only interact via the API using tokens. The owning user manages the bot's API tokens.
+
+The feature is gated behind a config flag `service.enablebotusers` (default: `false`).
+
+---
+
+## Data Model
+
+### User table changes
+
+Add one column to the existing `users` table:
+
+```go
+// In pkg/user/user.go, add to User struct:
+IsBot bool `xorm:"bool default false index" json:"is_bot"`
+```
+
+This is the only schema change to `users`. Bot users are regular rows in the `users` table with `is_bot = true`.
+
+Bot users:
+- Have a `username` (required, unique as usual)
+- Have a `name` (optional display name)
+- Have **no password** (empty string, never hashed)
+- Have **no email** (empty string — no notifications needed)
+- Use `Issuer = "local"` — but we skip the password/email validation for bots (see below)
+- Have `Status = StatusActive`
+- Have `EmailRemindersEnabled = false`
+- Have `OverdueTasksRemindersEnabled = false`
+
+### New table: `bot_owners`
+
+Tracks which human user owns which bot:
+
+```go
+// New file: pkg/models/bot_users.go
+type BotOwner struct {
+ ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
+ BotID int64 `xorm:"bigint not null unique index" json:"bot_id"` // FK → users.id (the bot)
+ OwnerID int64 `xorm:"bigint not null index" json:"owner_id"` // FK → users.id (the human)
+ Created time.Time `xorm:"created not null" json:"created"`
+}
+
+func (*BotOwner) TableName() string {
+ return "bot_owners"
+}
+```
+
+**Why a separate table instead of a field on `User`?**
+- Keeps the User struct clean — most code doesn't need to know about bot ownership
+- Allows querying "all bots owned by user X" efficiently
+- Avoids a self-referential FK on the users table
+
+---
+
+## Config
+
+Add to `pkg/config/config.go`:
+
+```go
+ServiceEnableBotUsers Key = `service.enablebotusers`
+```
+
+Default: `false`. Set in `InitDefaultConfig()`:
+
+```go
+ServiceEnableBotUsers.setDefault(false)
+```
+
+Add to `config-raw.json` under the `service` section:
+
+```json
+{
+ "key": "enablebotusers",
+ "default_value": "false",
+ "comment": "If enabled, users can create bot users that interact with Vikunja via the API only."
+}
+```
+
+Expose in the `/info` endpoint so the frontend knows whether to show bot-related UI:
+
+```go
+// In vikunjaInfos struct:
+BotUsersEnabled bool `json:"bot_users_enabled"`
+
+// In Info handler:
+info.BotUsersEnabled = config.ServiceEnableBotUsers.GetBool()
+```
+
+---
+
+## API Endpoints
+
+All bot endpoints require `service.enablebotusers` to be `true`. Return `403` (or a specific error) if disabled.
+
+### Create Bot
+
+**`PUT /api/v1/user/bots`**
+
+Request body:
+```json
+{
+ "username": "my-review-bot",
+ "name": "Code Review Bot"
+}
+```
+
+Handler logic:
+1. Check `config.ServiceEnableBotUsers.GetBool()` — return error if disabled
+2. Validate username (same rules: 1-250 chars, no spaces, not reserved)
+3. Create user with `IsBot = true`, no password, no email, `Status = Active`
+4. Skip email confirmation flow entirely
+5. Don't create an initial project for the bot (bots get added to projects manually)
+6. Insert `BotOwner{BotID: newUser.ID, OwnerID: currentUser.ID}`
+7. Return the created bot user
+
+Response:
+```json
+{
+ "id": 42,
+ "username": "my-review-bot",
+ "name": "Code Review Bot",
+ "is_bot": true,
+ "created": "2026-03-22T...",
+ "updated": "2026-03-22T..."
+}
+```
+
+### List My Bots
+
+**`GET /api/v1/user/bots`**
+
+Returns all bots owned by the current user. Supports search via `?s=` query param.
+
+Handler logic:
+1. Query `bot_owners WHERE owner_id = currentUser.ID`
+2. Join with `users` to get bot details
+3. Return list of bot users
+
+### Get Bot
+
+**`GET /api/v1/user/bots/:bot`**
+
+Returns a single bot by ID. Must be owned by current user.
+
+### Update Bot
+
+**`POST /api/v1/user/bots/:bot`**
+
+Update bot's `name` or `username`. Must be owned by current user.
+
+Request body:
+```json
+{
+ "name": "New Bot Name"
+}
+```
+
+### Delete Bot
+
+**`DELETE /api/v1/user/bots/:bot`**
+
+Soft-deletes (sets `Status = StatusDisabled`) or hard-deletes the bot user. Must be owned by current user.
+
+Considerations:
+- What happens to tasks the bot is assigned to? → Unassign the bot from all tasks on deletion.
+- What about comments the bot posted? → Keep them (same as when a human user is deleted).
+
+### Create API Token for Bot
+
+**`PUT /api/v1/user/bots/:bot/tokens`**
+
+Creates an API token owned by the bot user, but only the owning human can call this endpoint.
+
+Handler logic:
+1. Verify current user owns this bot (check `bot_owners`)
+2. Create an `APIToken` with `OwnerID = bot.ID`
+3. Return the token (visible only once, same as regular token creation)
+
+Request body:
+```json
+{
+ "title": "Main bot token",
+ "permissions": {
+ "tasks": ["read_all", "update"],
+ "task_comments": ["read_all", "create"]
+ },
+ "expires_at": "2027-01-01T00:00:00Z"
+}
+```
+
+### List Bot's API Tokens
+
+**`GET /api/v1/user/bots/:bot/tokens`**
+
+Lists API tokens belonging to the bot. Only the owning human can view these.
+
+### Delete Bot's API Token
+
+**`DELETE /api/v1/user/bots/:bot/tokens/:token`**
+
+Deletes an API token belonging to the bot. Only the owning human can call this.
+
+---
+
+## Auth Changes
+
+### Block password login for bots
+
+In `pkg/routes/api/v1/login.go`, after retrieving the user:
+
+```go
+if user.IsBot {
+ return &user2.ErrAccountIsBot{UserID: user.ID}
+}
+```
+
+Also in `CheckUserCredentials` (`pkg/user/user.go`) — bots have no password, so `bcrypt.CompareHashAndPassword` would fail naturally. But adding an explicit check is clearer and avoids the timing cost.
+
+### Block CalDAV auth for bots
+
+In `pkg/routes/caldav/auth.go`, reject bot users.
+
+### API token auth works as-is
+
+The existing API token middleware (`pkg/routes/api_tokens.go`) resolves the token owner and puts them in context. If the token belongs to a bot user, the bot user becomes the authenticated user. No changes needed here — the bot acts as itself.
+
+### User creation validation bypass for bots
+
+`checkIfUserIsValid()` in `pkg/user/user_create.go` currently requires password + username for local users and email for all. For bots:
+- Skip password requirement
+- Skip email requirement
+- Still require username
+
+This could be handled by either:
+- **Option A**: Adding a bot-specific code path in `checkIfUserIsValid()` that checks `IsBot`
+- **Option B**: Creating a separate `CreateBotUser()` function that skips those checks
+
+**Recommendation: Option B** — a separate `CreateBotUser()` function in `pkg/user/` that handles bot-specific creation logic cleanly without polluting the existing user creation path. It would:
+1. Validate username only
+2. Check uniqueness (same as regular users)
+3. Set `IsBot = true`, `Status = Active`, `EmailRemindersEnabled = false`, `OverdueTasksRemindersEnabled = false`
+4. Insert user
+5. Skip email confirmation, skip initial project creation
+6. Dispatch `CreatedEvent` as usual
+
+---
+
+## Notification Changes
+
+### Skip all email notifications for bots
+
+In `User.ShouldNotify()` (`pkg/user/user.go`):
+
+```go
+func (u *User) ShouldNotify(sessions ...*xorm.Session) (bool, error) {
+ // ... existing session setup ...
+ user, err := getUser(s, &User{ID: u.ID}, true)
+ if err != nil {
+ return false, err
+ }
+
+ if user.IsBot {
+ return false, nil
+ }
+
+ return user.Status != StatusDisabled && user.Status != StatusAccountLocked, err
+}
+```
+
+This is comprehensive — it prevents all notification types (email, overdue reminders, etc.) for bot users.
+
+---
+
+## User Search / Assignee Picker Changes
+
+### Bots appear in user search
+
+The `ListUsers()` function in `pkg/user/users_project.go` searches by username/name. Bot users will naturally appear in search results since they're regular users. No backend changes needed.
+
+### Bots appear in project member lists
+
+Bot users can be added to projects via the existing project user/team mechanisms. Once added, they appear in the assignee picker for that project's tasks. No changes needed to the project membership system.
+
+---
+
+## Frontend Changes
+
+### IUser type
+
+Add `isBot` to `frontend/src/modelTypes/IUser.ts`:
+
+```typescript
+export interface IUser extends IAbstract {
+ // ... existing fields ...
+ isBot: boolean
+}
+```
+
+### User model
+
+Add default in `frontend/src/models/user.ts`:
+
+```typescript
+isBot = false
+```
+
+### User.vue component — Bot badge
+
+Add a "Bot" badge next to the username when `user.isBot` is true:
+
+```vue
+
+
+
![]()
+
{{ displayName }}
+
Bot
+
+
+```
+
+Style the badge as a small pill/tag:
+
+```scss
+.bot-badge {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.65rem;
+ font-weight: 600;
+ padding: 0.1rem 0.35rem;
+ border-radius: 0.25rem;
+ background: var(--grey-200);
+ color: var(--grey-600);
+ margin-inline-start: 0.25rem;
+ vertical-align: middle;
+ text-transform: uppercase;
+ letter-spacing: 0.02em;
+}
+```
+
+This badge appears everywhere the `User` component is used: assignee lists, comment headers, task detail sidebar, etc.
+
+### Info store
+
+Add `botUsersEnabled` to the frontend's info/config store so components know whether to show bot-related UI (e.g., the "Manage Bots" settings page).
+
+### Bot management settings page
+
+New page at `frontend/src/views/user/settings/Bots.vue`:
+- List bots owned by current user
+- Create new bot (form: username, name)
+- Edit bot (update name)
+- Delete bot (with confirmation)
+- Manage bot API tokens:
+ - Create token (show once, copy to clipboard)
+ - List existing tokens
+ - Delete token
+
+This page only appears in the settings sidebar when `botUsersEnabled` is `true`.
+
+---
+
+## Database Migration
+
+Single migration adding:
+1. `is_bot` column to `users` table (bool, default false, indexed)
+2. `bot_owners` table
+
+```go
+func init() {
+ migrations = append(migrations, &xormigrate.Migration{
+ ID: "",
+ Description: "Add bot user support",
+ Migrate: func(tx *xorm.Engine) error {
+ // 1. Add is_bot column
+ err := tx.Exec("ALTER TABLE users ADD COLUMN is_bot BOOLEAN NOT NULL DEFAULT FALSE")
+ if err != nil { return err }
+
+ err = tx.Exec("CREATE INDEX IDX_users_is_bot ON users (is_bot)")
+ if err != nil { return err }
+
+ // 2. Create bot_owners table
+ type BotOwner struct {
+ ID int64 `xorm:"bigint autoincr not null unique pk"`
+ BotID int64 `xorm:"bigint not null unique index"`
+ OwnerID int64 `xorm:"bigint not null index"`
+ Created time.Time `xorm:"created not null"`
+ }
+ return tx.Sync2(&BotOwner{})
+ },
+ Rollback: func(tx *xorm.Engine) error {
+ return nil
+ },
+ })
+}
+```
+
+---
+
+## Error Types
+
+New error types in `pkg/user/error.go` (or `pkg/models/error.go`):
+
+| Error | Code | When |
+|-------|------|------|
+| `ErrAccountIsBot` | TBD | Bot tries to log in with password |
+| `ErrBotUsersDisabled` | TBD | Bot endpoint called when feature is off |
+| `ErrBotNotOwned` | TBD | User tries to manage a bot they don't own |
+| `ErrCannotMakeBotOwner` | TBD | Trying to set a bot as owner of another bot |
+
+---
+
+## What's NOT in scope (for now)
+
+These are deferred to the agent dispatch phase:
+
+- Agent endpoint configuration (OpenClaw/NanoClaw connection details)
+- Auto-dispatching work when a bot is assigned to a task
+- Agent status tracking
+- Health monitoring / timeout cron jobs
+- Bot-specific avatar generation (bots use the default avatar provider for now)
+
+---
+
+## Files to Create or Modify
+
+### New Files
+| File | Purpose |
+|------|---------|
+| `pkg/models/bot_users.go` | BotOwner model, bot CRUD logic |
+| `pkg/models/bot_users_permissions.go` | Permission checks for bot operations |
+| `pkg/routes/api/v1/bots.go` | Bot API endpoint handlers |
+| `pkg/migration/.go` | Database migration |
+| `frontend/src/views/user/settings/Bots.vue` | Bot management UI |
+| `frontend/src/services/botUser.ts` | Bot API service |
+
+### Modified Files
+| File | Change |
+|------|--------|
+| `pkg/user/user.go` | Add `IsBot` field to `User` struct |
+| `pkg/user/user.go` | Update `ShouldNotify()` to return false for bots |
+| `pkg/user/user_create.go` | Add `CreateBotUser()` function |
+| `pkg/config/config.go` | Add `ServiceEnableBotUsers` config key |
+| `config-raw.json` | Add config schema entry |
+| `pkg/routes/api/v1/info.go` | Expose `BotUsersEnabled` in `/info` |
+| `pkg/routes/api/v1/login.go` | Block bot login |
+| `pkg/routes/routes.go` | Register bot API routes |
+| `frontend/src/modelTypes/IUser.ts` | Add `isBot` field |
+| `frontend/src/models/user.ts` | Add `isBot` default |
+| `frontend/src/components/misc/User.vue` | Add bot badge |
+| `frontend/src/i18n/lang/en.json` | Add translation strings |
+
+---
+
+## Open Questions
+
+1. **Should bots be sharable?** Can user A transfer ownership of a bot to user B? Or share management access? For now: no. One owner per bot, non-transferable.
+
+2. **Hard delete vs soft delete?** When a bot is deleted, should we hard-delete the user row or set `Status = Disabled`? Soft delete (disabled) is safer since it preserves audit trail and comment attribution. Recommend soft delete.
+
+3. **Bot-to-bot ownership?** A bot should not be able to own another bot. The owner must be a human user (`is_bot = false`). Enforce in the creation handler.
+
+4. **Bot API token scopes — should we restrict?** Should bots be able to have tokens with any permission, or only a subset? For now: same as regular users — any valid permission scope. The owning user decides what the bot can do.
+
+5. **Rate limiting for bot API calls?** Not in this phase. Can be added as a config option later (`service.botratelimit`).