Fieldforce Phase 5 — AI Risk Intervention Inbox Design
Phase 5 ships a manager-facing AI Risk Intervention Inbox that turns risk detection into actionable draft interventions — assignee nudges, supervisor escalations, or reassignment recommendations. A daily cron pipeline deterministically scores open tasks, persists ranked recommendation rows, and calls the LLM once per org to draft text; page loads serve stored rows only and never trigger LLM calls.
Phase 5 ships a manager-facing AI Risk Intervention Inbox that turns daily risk detection into actionable draft interventions. A cron pipeline deterministically scores open tasks, persists ranked recommendation rows, and calls the LLM once per org to draft intervention text — page loads serve stored rows only and never trigger LLM calls.
Scope & Goals
Phase 5 supports three recommendation types:
Assignee Nudge
assignee_nudgeDraft a short message asking the field team member to unblock, update, or complete the task.
Supervisor Escalation
supervisor_escalationDraft a message asking the supervisor to intervene.
Reassignment Recommendation
reassignRecommend task reassignment with draft rationale. Requires LLM reasoning — never emitted heuristically.
Success Criteria
- A manager can open one page and see the highest-risk open tasks without scanning the full task list.
- Each open item explains the risk, why it matters, the recommended action, and a usable draft intervention.
- A manager can request a bounded rewrite of the draft, dismiss the recommendation, or manually mark it as acted on.
- Gate denial or LLM failure still leaves a useful heuristic-only risk card.
- The feature is observable and kill-switchable through the established Fieldforce and AI controls.
Out of Scope
- Automatic message sending, task reassignment, escalation, or status transition.
- Verifying that the manager actually performed the intervention outside the inbox.
- Free-form AI chat over Fieldforce data.
- Route optimization, workload balancing, approval/photo quality scoring, weekly performance insights.
- Email, WhatsApp, or push delivery of interventions.
- Field-team mobile AI assistant.
Approach Selected
Briefing Follow-up Drafts
- Strength
- Smallest scope; builds directly on Phase 4 at-risk rows
- Cost
- Only works from daily briefing context
Rejected — the user selected a broader proactive surface not dependent on opening a briefing.
Risk Intervention Inbox
- Strength
- Best manager command-center experience; proactive and filterable
- Cost
- New persisted recommendation rows and UI route
New /app/fieldforce/risk-inbox panel surface backed by persisted ff_risk_interventions rows. Reuses Phase 4 risk signals where available but is not dependent on them.
Task-Detail Copilot
- Strength
- Simple and contextual for one task
- Cost
- Not proactive; weak for managers scanning many risks
Rejected for MVP. Natural Phase 6 addition as a contextual follow-on.
Architecture & Module Layout
Risk interventions are a sub-feature of the existing fieldforce module. No new top-level module is introduced. The generation job follows the OverdueJob pattern — the job struct owns its ticker and is wired from module.go. No adapter/inbound/cron/ layer.
backend/go/internal/modules/fieldforce/
domain/entity/
risk_intervention.go
application/port/
risk_intervention_ports.go
application/usecase/
generate_risk_interventions.go # Start(ctx); wired from module.go like OverdueJob
risk_candidate_scoring.go # pure — no DB access
risk_intervention_query.go
risk_intervention_followup.go
adapter/inbound/http/
risk_intervention_handler.go
adapter/outbound/persistence/postgresql/
risk_intervention_repository.go
- AccessGate feature key
fieldforce:risk_intervention(generation + follow-up rewrites)- DB-managed prompts
ai_prompt_templatesforfieldforce:risk_intervention, localesen/zh/ms. ADR-0025: no code-side fallback.- Feature flag key
fieldforce.risk_interventions, seeded globallyfalse(ADR-0016)- Org module config
org_module_configsunder modulefieldforce
Generation Flow
LLM Output Schema
One call per org tick. Returns an array; one item per shortlisted task. risk_score and risk_reasons are never taken from LLM output — they come from the deterministic step only.
[
{
"task_id": "<uuid from shortlist>",
"recommended_action": "assignee_nudge | supervisor_escalation | reassign",
"risk_summary_md": "<one-paragraph manager-facing explanation>",
"draft_text": "<ready-to-send intervention draft>"
}
]
Risk Scoring Rules
Scores are additive — a task can fire multiple rules. Candidates are ranked by risk_score DESC; the top shortlist_max (default 10) are sent to the LLM. The rule key list is a stable API contract between the scoring engine and UI chip labels — new keys require a spec update and i18n entry.
-
overdueDueDate < now AND status NOT IN (approved, cancelled)40 -
escalated_no_progressEscalatedAt IS SET AND no activity logged since escalation30 -
repeated_rejectionlen(ReviewNotes) >= 2— stuck in needs_revision cycle 25 -
approaching_dueDueDate within 24h AND status NOT terminal20 -
no_activitystatus = in_progress AND no activity logged in last 48h20 -
high_priority_idlepriority IN (urgent, high) AND status = pending15 -
stalled_checklisthas checklist items AND none completed AND status = in_progress10
Follow-up Flow
-
Handler: validate revision_type (syntactic)
Accepts:
shorter,softer,firmer,translated,explanation. Returns 400 on unknown type. -
Use case: check followups_enabled org config (policy)
Reads
org_module_configs[fieldforce][risk_interventions.followups_enabled]. Returns 403 if disabled. -
AccessGate.Authorize
Feature key
fieldforce:risk_intervention. Gate denial returns error — no LLM call. -
LLM rewrites current draft text
Narrow, bounded operation — not free-form chat. Transforms the existing recommendation only.
-
Append revision row
Saves to
ff_risk_intervention_revisionswithrevision_typeand newdraft_text.
Data Model
ff_risk_interventions
One row per (org_id, task_id, risk_date). Dismissal scope is one calendar day — a still-risky task generates a new row the next day regardless of prior terminal status.
| Column | Type | Notes |
|---|---|---|
id | uuid PK | |
org_id | text NOT NULL | tenant scope |
task_id | uuid NOT NULL | no hard FK in MVP |
risk_date | date NOT NULL | org-local date of generation |
status | text NOT NULL | open, dismissed, acted_on, expired |
risk_score | int NOT NULL | deterministic additive sum of rule weights |
risk_reasons | jsonb NOT NULL | array of rule keys |
recommended_action | text NOT NULL | assignee_nudge, supervisor_escalation, reassign |
risk_summary_md | text NOT NULL | always populated; Go-template for heuristic rows, LLM prose for llm rows |
draft_text | text | null for heuristic-only rows |
generation_mode | text NOT NULL | pending, llm, heuristic_only |
prompt_template_version | int | null for heuristic rows |
assigned_team_ids | text[] NOT NULL | snapshotted at generation time for supervisor filtering |
assignee_user_ids | text[] NOT NULL | snapshotted display/filter context |
created_at | timestamptz NOT NULL | |
updated_at | timestamptz NOT NULL | |
dismissed_at | timestamptz | set on dismiss |
acted_on_at | timestamptz | set on manual acted-on |
- Unique constraint
(org_id, task_id, risk_date)- Inbox index
(org_id, status, risk_score DESC, created_at DESC, id DESC)- Task-detail index
(task_id)— for future task-detail embedding- Keyset cursor
- opaque base64 on
(risk_score, created_at, id)— stable when scores repeat
ff_risk_intervention_revisions
Append-only draft versions. Never updated after insert.
| Column | Type | Notes |
|---|---|---|
id | uuid PK | |
intervention_id | uuid NOT NULL | parent intervention |
revision_type | text NOT NULL | original, shorter, softer, firmer, translated, explanation |
draft_text | text NOT NULL | |
created_by | text NOT NULL | user ID |
created_at | timestamptz NOT NULL |
API
GET /api/organizations/:org_id/fieldforce/risk-interventions
GET /api/organizations/:org_id/fieldforce/risk-interventions/:id
POST /api/organizations/:org_id/fieldforce/risk-interventions/:id/follow-up
POST /api/organizations/:org_id/fieldforce/risk-interventions/:id/dismiss
POST /api/organizations/:org_id/fieldforce/risk-interventions/:id/mark-acted-on
List Filters
- status
open(default),dismissed,acted_on,expired- team_id
- filter by assigned team
- risk_reason
- filter by rule key (e.g.
overdue,no_activity) - recommended_action
assignee_nudge,supervisor_escalation,reassign- min_score
- minimum risk_score threshold
- cursor
- opaque base64 keyset cursor on
(risk_score, created_at, id); offset pagination not supported
Default sort: risk_score DESC, created_at DESC, id DESC.
Role Behavior
Admin / Owner / Manager
org-wideSee all org recommendations. No team filtering applied.
Supervisor
team-scopedSees recommendations where snapshotted assigned_team_ids intersects their current teams (ADR-0017 member scan at read time).
Field Team
403Route returns 403. Panel route is hidden. Not their surface.
Status Transitions
Panel Experience
- Inbox list
/app/fieldforce/risk-inbox- Detail view
/app/fieldforce/risk-inbox/:id
This task is overdue and no activity has been logged in the past 48 hours. The assignee may be blocked — a nudge to unblock, update, or complete is the recommended next step.
The detail view shows the risk summary, deterministic reasons, AI draft text (if available), revision history, bounded rewrite controls (shorter / softer / firmer / translated / explanation), copy/edit affordance, and dismiss/acted-on actions.
Configuration, Flags & Prompts
Org Module Config Keys
Key (module fieldforce) | Default | Purpose |
|---|---|---|
risk_interventions.schedule_local_time | "08:30" | local daily generation time |
risk_interventions.timezone | "UTC" | IANA timezone for schedule |
risk_interventions.shortlist_max | 10 | max candidates sent to LLM |
risk_interventions.retention_days | 60 | how long terminal rows are kept |
risk_interventions.expire_after_hours | 48 | age-based expiry threshold for stale open rows |
risk_interventions.followups_enabled | true | controls bounded rewrite endpoint and UI |
Prompt templates seeded at migration: fieldforce:risk_intervention for locales en, zh, ms. Runtime reads only from ai_prompt_templates. If prompt lookup fails, generation falls back to heuristic-only and records an AccessGate error (ADR-0025 — no code-side LLM prompt fallback).
Error Handling
- AccessGate denies generation: persist heuristic-only rows; no AI draft text.
- LLM failure: record error; persist heuristic-only rows.
- LLM references unknown task ID: drop that output row.
- LLM uses unsupported recommended_action: drop that output row.
- All LLM rows fail validation: persist heuristic-only rows for the full deterministic shortlist.
- Follow-up revision_type unsupported: return 400.
- Follow-up disabled by org config: return 403.
- Caller cannot access org or team scope: return existing Fieldforce 403/404 convention.
- Duplicate generation (same task/date): UNIQUE conflict — keep existing row, no duplicate draft appended.
Testing Strategy
Backend
- Unit tests for deterministic risk scoring: all 7 rule keys, additive accumulation, edge cases (nil DueDate, empty Activities).
- Unit tests for status transitions: dismiss, manual acted-on, expire.
- Use-case tests: AccessGate denied, LLM success, invalid LLM output (bad task_id, bad action), all-fail fallback, duplicate generation, expiry sweep, dismiss, manual acted-on.
- Repository tests: unique constraint, keyset cursor pagination, terminal status filtering, revision append, supervisor team-scope filtering.
- HTTP tests: field-team 403, role gates, filter parsing, follow-up validation (bad type → 400, disabled → 403), dismiss, manual acted-on.
Frontend
- Component tests: inbox list rendering, reason chips, action badges, filters, empty states, heuristic-only rows (no draft controls).
- Component tests: detail view, revision history, bounded follow-up controls, dismiss, manual acted-on.
- Hook tests: list/detail/follow-up/dismiss/acted-on requests.
- Playwright happy path: seeded open risk item → manager opens inbox → requests shorter revision → marks acted on → item leaves default open list.
Verification Goals
- Page loads never trigger LLM calls.
- AI failure paths still render actionable heuristic risk cards.
- Manual acted-on updates status and sets
acted_on_at— does not call any task mutation endpoint. - Supervisors cannot read risk items outside their team scope.
- Pending rows never appear in list or detail responses.
Implementation Notes
- Keep risk scoring pure and independent from persistence —
risk_candidate_scoring.gohas no DB access. - Use persisted rows as the API boundary; never recompute inbox content on reads.
- Do not introduce a generic chat abstraction for follow-ups — the endpoint transforms an existing recommendation only.
- Use append-only revisions so draft changes and manager decisions remain explainable.
- Reuse Phase 4 local-time scheduling helper if it exists by implementation time. If Phase 4 is not yet merged, implement the helper once in the fieldforce module for both phases to share.
- Keep execution actions manual in MVP. Real message-send or reassignment links require their own authorization and audit design in a later phase.