---
url: /kanban/workflow.md
description: >-
  Allowed Kanban transitions, WIP limits, custom rules, after-move hooks and
  policy-based permissions.
---

# Workflow Rules

## Allowed moves

Implement `HasKanbanTransitions` on the status enum:

```php
enum TaskStatus: string implements HasColor, HasIcon, HasKanbanTransitions, HasLabel
{
    public function getTransitions(): array
    {
        return match ($this) {
            self::Backlog => [self::Todo],
            self::Todo => [self::Backlog, self::InProgress],
            self::InProgress => [self::Todo, self::Review],
            self::Review => [self::InProgress, self::Done],
            self::Done => [self::Review],
        };
    }
}
```

Or define them on the board:

```php
->transitions(['todo' => [TaskStatus::InProgress]])
```

Columns you don't list accept anything. Refused moves are flagged while you drag, before you drop:

## WIP limits

```php
->wipLimits(['in_progress' => 3])
```

The header shows `3 / 3`, amber at the limit and red above it. Limits warn by default, like Jira. Refuse moves into a full column:

```php
->wipLimits(['in_progress' => 3], enforce: true)
// or
->enforceWipLimits()
```

## Your own rules

```php
->validateUsing(fn (Task $record, TaskStatus $to, ?Member $lane): ?string => ...)
```

Return a message to refuse a move.

## Hooks

```php
->afterMove(fn (Task $record, TaskStatus $from, TaskStatus $to) => ...)
```

Runs after a move is saved, for notifications or activity logs.

## Permissions

With a policy on the model, `update` controls dragging and editing, `create` controls adding, and `delete` the delete action. Locked cards show a lock and can't be picked up.

Every rule runs on the server for drags, keyboard moves, inline adds and both modals.
