Кейсы (case-aware workflows)¶
Case — project-scoped сущность-носитель бизнес-кейса (клиент, сделка, заявка, тикет), вокруг которого крутятся workflow-запуски. Хранит идентификаторы (
identity_keys), произвольные свойства (properties), теги, ссылки на внешние сущности (Telegram chat, email thread, Google Drive folder), статус (active/archived). Сами файлы Axon durably не хранит — доступ к содержимому идёт через connector layer по ссылкам изproperties.file_sources. Канон:CONCEPT-CASE-WORKFLOWS.md§3-§4.
1. Что это и зачем¶
Workflow в Axon — единица исполнения; case — единица контекста. Один и тот же workflow customer_support_triage запускается на разных кейсах (case_001 = клиент A, case_002 = клиент B), а кейс собирает на себе все связанные run'ы, артефакты, переписку, файлы. Это даёт:
- Историю по сущности. Все runs
WHERE case_id = ?— что когда-либо делалось для этого клиента. - Идентичность.
identity_keys(например,{"email": "x@y.com"}или{"crm_id": "CRM-123"}) — детерминистический ключ дедупликации: входящий ивент (новое письмо, обращение) находит существующий кейс по identity_keys, не плодит дубликаты. - Контекст для шагов. Step в workflow может читать
case.properties(свойства),case.external_refs(привязки),case.properties.file_sources(где лежат файлы) — это «база знаний» по кейсу. - Case-level overrides коннекторов (D17 средний слой). Например, project-wide binding profile говорит «slot
gmail_inbox→ инстансgmail_support», но конкретный кейс может переопределить: «именно для этого VIP-клиента —gmail_vip». Run-level override (третий слой) перебивает case-level.
Канонический рассказ — CONCEPT-CASE-WORKFLOWS.md. Этот мануал описывает UI и операционный путь.
2. Роли и доступ¶
Из core/models/auth.py. Полная матрица — Roles-And-Permissions.md.
| Действие | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| Создать кейс | create_case |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
Редактировать properties / tags / identity_keys |
edit_case |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Архивировать / разархивировать кейс | archive_case |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Привязать external ref (Telegram chat, email thread, GDrive folder) | attach_case_external_ref |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Удалить external ref | delete_case_external_ref |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Запустить workflow для кейса (Run Wizard от кейса) | start_workflow |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | — |
| Читать кейс | read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
Важно:
managerимеетattach_case_external_ref, но не имеетdelete_case_external_ref— удаление привязки (потенциально разрушительная операция, теряется контекст) поднято до admin/owner. Operator — только чтение и запуск run'ов в рамках своих project_scopes.
Все мутации идут через CommandEnvelope через канонические handlers в core/api/commands/handlers/cases.py (CreateCaseHandler, UpdateCasePropertiesHandler, UpdateCaseStatusHandler, ArchiveCaseHandler, AttachExternalRefHandler, DeleteExternalRefHandler). Backdoor writes запрещены (см. ARCHITECTURE-V6 §17 RBAC, инвариант 5 VISION).
3. Где это в Console¶
Раздел сайдбара Cases в каждом проекте (URL: /{project_id}/cases).
⚠️ Раздел скрыт за фича-флагом
VITE_CONSOLE_STAGE3_UI_ENABLED. Сейчас флаг не прокинут в console Dockerfile build args — в стандартном образе раздел не виден. Включается build-time:--build-arg VITE_CONSOLE_STAGE3_UI_ENABLED=true(или runtime черезCONSOLE_STAGE3_UI_ENABLED=true, если console-сервер это поддерживает). См. §8 Траблшутинг.
| Экран | Что на нём |
|---|---|
Cases list (/{project_id}/cases) |
таблица: case_id префикс, display_name, case_type, status (active/archived), tags, updated_at. Фильтры: по case_type, по статусу, поиск по identity_keys (через GIN-индекс). Кнопка + New case. |
Case create (/{project_id}/cases/new) |
форма: case_type (snake_case, ^[a-z][a-z0-9_]*$), display_name, identity_keys (JSON), tags (массив), properties (JSON textarea). После создания — редирект на detail. |
Case detail (/{project_id}/cases/{case_id}) |
вкладки/секции: Identity (case_id, type, identity_keys, status), Properties (read-only JSON view), Tags, External refs (список Telegram chat / email thread / GDrive folder / др. с кнопкой Detach для admin/owner), Connector overrides (D17, picker если есть workflow с binding profile), File sources (свойство properties.file_sources — список FileSourceRef), Workflow runs (все runs, у которых case_id = текущий), Actions: Edit, Archive, Start workflow (открывает Run Wizard в режиме «от кейса» — case_id подставлен). |
Case edit (/{project_id}/cases/{case_id}/edit) |
редактирование display_name, properties (JSON), tags, identity_keys. Изменения идут через update_case_properties (для properties/tags/identity_keys/display_name). |
JSON-редактор properties — обычный textarea с валидацией формы (parseJsonObject / formatJson utility). Никакого schema-driven form builder в текущей версии нет — задача владельца проекта валидировать форму свойств в коде (publish-time input_schema валидатор для case-aware workflows проверяет, что case.properties соответствует ожидаемой shape).
4. Концепции (mental model)¶
Project ── 1 : N ── Case ── 1 : N ── Workflow run (case_id FK)
│
├── identity_keys (JSON) ─── детерминистическая дедупликация
├── properties (JSON) ─── произвольные поля + file_sources
├── tags (array) ─── ярлыки для фильтрации
├── external_refs (1:N) ─── Telegram chat, email thread, GDrive folder
├── connector_overrides (D17) ─── case-level override binding profile
└── status: active | archived
identity_keys — JSON-объект, например {"email": "lead@acme.com"} или {"crm_id": "CRM-123", "channel": "telegram"}. Уникальность опциональная (зависит от типа кейса). Поиск по identity_keys использует GIN-индекс cases_identity_keys_gin_idx. Ивент-роутер при входе пытается найти кейс по identity_keys прежде чем создать новый.
properties — произвольный JSON object под бизнес-данные кейса. Свободная форма, но есть convention:
properties.file_sources: list[FileSourceRef]— стандартный способ объявить, где лежат файлы кейса (Google Drive folder, Dropbox, и т.д.). См. §6.2. Сами файлы Axon не хранит — только ссылку и connector binding. Доступ к содержимому — через connector layer (Google Drive adapter и т.д.).properties.connector_overrides: dict[str, str](D17 средний слой) — case-scope override binding profile проекта. Формат{"requirement_key": "ci_<ulid>"}. Валидируется shape-only вvalidate_case_properties_connector_overrides()(имя слота + форматci_<ulid>); семантическая проверка (CI существует, того же проекта, usable) выполняется наstart_workflowчерез binding resolver (D45 защита в глубину).
tags — массив строк, чисто UI-overlay для фильтрации (по аналогии с workflow labels, см. CONCEPT-WORKFLOW-LABELS.md §1). Execution layer не читает tags, не маршрутизирует по ним и не меняет поведение.
status — два значения: active / archived. Архивация — это status transition, не удаление; кейс остаётся в БД для аудита, истории и compliance. Архивированные кейсы скрыты в UI по умолчанию, но доступны через фильтр статуса.
external_refs — отдельная таблица (1:N к case). Каждая запись: external_ref_id (префикс xref_), kind (telegram_chat / email_thread / gdrive_folder / др.), ref_value (опаковый id у провайдера), display_name. Используется, чтобы связать кейс с внешней сущностью без проникновения в properties (схема + права на удаление под admin/owner).
case_aware workflows. Workflow с case_aware=true (флаг в metadata) обязательно стартует с case_id. Publish-time валидатор проверяет, что определение объявляет case_aware корректно и что input_schema совместима с case-aware shape. Run Wizard в этом режиме требует выбор кейса перед Create + Start. См. WORKFLOW-ARCHITECTURE.md §4.
5. Флоу: пошаговые сценарии¶
5.1 Создать кейс вручную¶
manager+идёт в Cases → + New case.- Заполняет
case_type(snake_case, напримерcustomer_lead/support_ticket),display_name, опциональноidentity_keys(JSON),tags,properties. Совет: заполняйidentity_keysсразу — это ключ дедупликации; пустой объект разрешён, но тогда ивент-роутер не сможет найти кейс при следующем входящем ивенте. - Submit →
CreateCaseHandler(create_case, sync_apply,internal) → INSERT вapp.cases, генерируетсяcase_id = case_<ulid>. Outbox-событиеcase.created(для ивент-роутеров и read-projection rebuild). Редирект на detail.
5.2 Запустить workflow для кейса (Run Wizard от кейса)¶
- На Case detail → Start workflow. Открывается Run Wizard,
case_idуже подставлен и заблокирован для смены. - Выбираешь
WorkflowDefinition(среди опубликованных case-aware definitions проекта). - Заполняешь
input_data(форма строится поinput_schemadefinition'а). - Опционально override bindings (run-level, перебивает case-level и project-level): для multi-credential workflow выбираешь конкретный CI на каждый
connector_requirement. - Create + Start →
create_workflow+start_workflow(два command'а). Наstart_workflowsystem actorworkflow_start_dispatcher(sub-steppin_workflow_run_bindings) пиннит финальный snapshot bindings: профиль → case_overrides → run_overrides. Snapshot сохраняется вapp.workflow_run_bindings_snapshotи читается каждым step'ом при исполнении.
5.3 Привязать external ref (Telegram chat, email thread, GDrive folder)¶
- На Case detail → External refs → + Attach.
- Выбираешь
kind(telegram_chat,email_thread,gdrive_folder, ...), вводишьref_value(chat id / thread id / folder URL),display_name. - Submit →
AttachExternalRefHandler(attach_external_ref, sync_apply). Запись вcase_external_refs, outbox-событиеcase.external_ref_attached. - Для GDrive: используй удобный URL —
parse_gdrive_folder_url()извлечётfolder_id. Это не подменяетproperties.file_sources(это разные слои: external_refs — общая привязка, file_sources — конвенция для коннекторного слоя).
5.4 Архивировать кейс¶
- На Case detail → Actions → Archive (или массово через Cases list, если поддержано).
- Подтверждение →
ArchiveCaseHandler(archive_case, sync_apply). UPDATEapp.casesstatus = 'archived',archived_at = now(), archived_by_actor_id. Canonical timestamp — вaudit.audit_log(event_type='case.archived'). - Что происходит с активными runs кейса: активные runs не отменяются автоматически. Архивация — это бизнес-сигнал «кейс завершён», а не системный teardown. Если нужно остановить runs — это отдельные
cancel_workflowкоманды (по списку run'ов кейса). Это сознательное разделение: undo и status transitions — независимые контракты (VISION инвариант 3). - Разархивация: через
update_case_status(status='active')тем же handler-ом (UpdateCaseStatusHandler), если это разрешено бизнес-политикой; в стандартной поставке archived — terminal в UI, но не в БД.
5.5 Удалить external ref¶
admin+идёт Case detail → External refs → ⋯ → Delete (видно только для admin/owner: у manager этой кнопки нет — RBAC enforced server-side + UI-gate).- Подтверждение →
DeleteExternalRefHandler(delete_external_ref, sync_apply).
6. Справочник опций¶
6.1 Поля кейса (app.cases)¶
| Поле | Тип | Описание |
|---|---|---|
case_id |
text PK |
префикс case_, формат case_<26-char ULID, Crockford>. CHECK constraint: case_id ~ '^case_[0-9a-hjkmnp-tv-z]{26}$'. |
project_id |
text FK |
NOT NULL; cross-project leak запрещён на уровне БД (FK + composite checks). |
case_type |
text |
snake_case, 1-64 символа, ^[a-z][a-z0-9_]*$. Свободный business-driven type. |
display_name |
text |
человекочитаемое имя. |
identity_keys |
jsonb |
объект (jsonb_typeof = 'object'), default {}. Идентичность кейса; ключ дедупликации; GIN-индекс по полю. |
properties |
jsonb |
произвольный object; convention поля — file_sources и connector_overrides (см. ниже). |
tags |
text[] |
UI overlay. |
status |
text |
'active' или 'archived' (CHECK constraint). |
archived_at / archived_by_actor_id |
timestamp / text | заполняются при archive_case. |
created_at / created_by_actor_id |
timestamp / text | audit fields. |
updated_at / updated_by_actor_id |
timestamp / text | audit fields. |
version |
bigint |
optimistic concurrency (используется expected_version в CommandEnvelope). |
6.2 FileSourceRef (convention properties.file_sources[*])¶
Источник: core/models/file_sources.py.
| Поле | Тип | Описание |
|---|---|---|
kind |
'gdrive_folder' \| ... |
тип источника; должен соответствовать adapter_key привязанного коннектора |
display_name |
str (1-255) |
человекочитаемое имя для UI |
folder_id |
str |
provider-specific id (для GDrive — folder ID) |
folder_url |
str (опц.) |
удобный URL; валидация проверяет, что folder_id соответствует URL (_folder_id_matches_url) |
Хелпер parse_gdrive_folder_url() извлекает folder_id из URL вида https://drive.google.com/drive/folders/<id> (в т.ч. с query-параметрами).
Файлы Axon durably не хранит. FileSourceRef — это ссылка на folder у провайдера. Содержимое читается коннектором (Google Drive adapter и т.д.) в момент исполнения шага workflow. Удаление кейса не удаляет файлы у провайдера; удаление папки у провайдера не отражается в Axon (нужен health-probe на стороне коннектора).
6.3 External ref kinds¶
case_external_refs.kind — открытое множество строк (без CHECK enum), но в текущей версии используются: telegram_chat, email_thread, gdrive_folder, slack_channel (если slack-коннектор активен). Новые kind'ы добавляются по мере появления адаптеров.
6.4 connector_overrides (D17 case-level средний слой)¶
case.properties.connector_overrides: dict[str, str] (опционально).
Формат: {"<requirement_key>": "ci_<ulid>"}. Каждый ключ — имя слота из WorkflowDefinition.connector_requirements[*].requirement_key; значение — connector_instance_id (см. Connectors-Credentials.md §6.2).
Shape валидируется в validate_case_properties_connector_overrides() при update_case_properties (имя слота non-empty, ID соответствует формату ci_<26-char ULID>). Семантическая проверка (CI существует, того же проекта, usable — D42) выполняется binding resolver-ом на start_workflow через resolve_workflow_run_bindings() (D45 defense-in-depth, не доверяет валидаторам случая).
7. Жизненный цикл и обслуживание¶
Статусный автомат: active → archived (archive_case); опционально archived → active (update_case_status, если бизнес-политика позволяет). Canonical timestamp — audit.audit_log event case.archived / case.unarchived.
Активные runs кейса при архивации не отменяются автоматически (см. §5.4).
External refs живут отдельной таблицей; удаление кейса (если допущено — в текущей версии не реализовано, см. §9) каскадно удалит ссылки, но не сами объекты у провайдера.
GDPR-каскад при удалении проекта. При archive_project / project teardown — кейсы проекта получают cascade (audit.case.cascade_archived_by_project); полный erase данных (личной информации в properties / identity_keys) выполняется отдельным GDPR sweeper-ом (см. CONCEPT-CASE-WORKFLOWS.md §7 GDPR helpers — typed DTOs shared с Zone 3).
Read-projection read.case_registry (alembic 035) rebuildable; перестраивается из app.cases outbox-консьюмером (refresh/case_registry.py). При проблемах с UI — axon refresh-read-models case_registry (см. Security-And-Audit.md или CLI).
8. Траблшутинг¶
| Симптом | Причина | Что делать |
|---|---|---|
| Раздел Cases не виден в сайдбаре | VITE_CONSOLE_STAGE3_UI_ENABLED не включён на сборке console (Dockerfile default — false) |
Пересобрать console-образ с --build-arg VITE_CONSOLE_STAGE3_UI_ENABLED=true (см. console/Dockerfile). Если используется runtime override — проверить CONSOLE_STAGE3_UI_ENABLED=true в env. |
| Кнопка Delete external ref не видна | Нет permission delete_case_external_ref (только admin/owner) |
Эскалировать до admin/owner или использовать archive_case если связь больше не нужна. |
update_case_properties возвращает 422 «connector_overrides shape invalid» |
properties.connector_overrides содержит не-string ключи или value не в формате ci_<26-char ULID> |
Проверить shape: {"slot_name": "ci_01HXXX..."}. Не путать с cred_<ulid> (это credential_id, не CI). |
start_workflow от кейса валится с binding_resolver: case_override references unknown CI |
Case-level override указывает на удалённый / cross-project / unusable CI | Открыть Case detail → Connector overrides, обновить выбор или удалить override (вернётся в project binding profile). |
| Ивент-роутер плодит дубли кейсов на одинаковый входящий email | identity_keys не заполнены / неконсистентны между источниками |
Заполнить identity_keys = {"email": "<addr>"} для существующих и обновить роутер (или business workflow), чтобы он клал тот же ключ. |
Кейс не находится по identity_keys через поиск Console |
Не использован GIN-индекс — поиск идёт по подмножеству ключей (containment @>) |
Сейчас Console использует prefix-search по display_name + точный match по identity_keys; для произвольного query — Query API GET /api/v1/query/cases?identity_keys=<json>. |
| После архивации кейса остались pending approvals от его workflow runs | Архивация не отменяет runs (см. §5.4) | Отдельно cancel_workflow для каждого run'а либо завершить approvals руками. |
9. Ограничения и инварианты¶
project_id NOT NULL. Кейс всегда принадлежит ровно одному проекту. Cross-project FK / refs запрещены на уровне БД и валидируются handler-ом.- Файлы не хранятся durably.
file_sources— только ссылки; content — у провайдера, доступ через connector layer. Axon не реплицирует и не кэширует содержимое (за исключением transient artefacts конкретного step'а). - Mutate только через canonical handlers.
CreateCase,UpdateCaseProperties,UpdateCaseStatus,ArchiveCase,AttachExternalRef,DeleteExternalRef. Backdoor SQL UPDATE — нарушение VISION инварианта 5 (CommandEnvelope — единственный путь мутации). identity_keys—jsonb_typeof = 'object'(CHECK constraintcases_identity_keys_object). Массивы / скаляры отвергаются.case_typeregex^[a-z][a-z0-9_]*$, 1-64 символа (CHECK constraintcases_case_type_format).status ∈ {'active', 'archived'}(CHECK constraintcases_status_values). Никаких других значений; pending/in_progress/closed/etc. — это бизнес-семантика для workflow runs или дляproperties.business_status, не для самого кейса.- D17 case-level override — only
connector_overrides. В текущей версии случай переопределяет binding profile только черезproperties.connector_overridesshape. Произвольное «case overrides любое поле run'а» — не контракт. - Secrets никогда не в
properties.core/security/credential_scan.pyотвергаетupdate_case_propertiesеслиpropertiesсодержит секрето-подобные строки (токены/ключи/credential URLs). Это инвариант: case — публичный бизнес-контекст, не credential store. - Удаление кейса (hard delete) — beyond Stage 3. В текущей версии есть только archive (soft transition). Hard delete + GDPR purge — отдельный backlog (см. CONCEPT-CASE-WORKFLOWS.md §7 + OWNERSHIP-MAP Zone 5 sweepers).
10. Связанные мануалы и каноны¶
- Workflows.md — определения / Run Wizard / binding profiles; case-aware workflows запускаются именно отсюда.
- Connectors-Credentials.md — connector instances; case-level overrides (D17) указывают на CI.
- Approvals.md — как утверждать шаги в workflow run'е кейса.
- Undo-And-Compensation.md — что значит «отменить шаг» в контексте case-aware workflow.
- Security-And-Audit.md — audit-events
case.created/case.archived/case.external_ref_*. - Каноны:
CONCEPT-CASE-WORKFLOWS.md§3 (case как атом контекста), §4 (file_sources convention), §7 (GDPR), §10.2 (FileSourceRef);ARCHITECTURE-V6.md§11 (cases ↔ connector layer);WORKFLOW-ARCHITECTURE.md§4 (case-aware workflows).