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.

View source markdown ↗ generated by claude-sonnet-4-6 · diagrams mermaid

  • Phase Fieldforce 5
  • Audience Managers + supervisors (team-scoped)
  • Surface /app/fieldforce/risk-inbox
  • AI role Drafting authority only — no automated actions
  • LLM calls 1 per org per day (cron, never on page load)

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_nudge

Draft a short message asking the field team member to unblock, update, or complete the task.

Supervisor Escalation

supervisor_escalation

Draft a message asking the supervisor to intervene.

Reassignment Recommendation

reassign

Recommend 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

Rejected

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.

Selected

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.

Rejected

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_templates for fieldforce:risk_intervention, locales en / zh / ms. ADR-0025: no code-side fallback.
Feature flag key
fieldforce.risk_interventions, seeded globally false (ADR-0016)
Org module config
org_module_configs under module fieldforce

Generation Flow

Daily risk intervention generation pipeline — lease-then-LLM per ADR-0023
Daily risk intervention generation pipeline — lease-then-LLM per ADR-0023

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.

  • overdue DueDate < now AND status NOT IN (approved, cancelled) 40
  • escalated_no_progress EscalatedAt IS SET AND no activity logged since escalation 30
  • repeated_rejection len(ReviewNotes) >= 2 — stuck in needs_revision cycle 25
  • approaching_due DueDate within 24h AND status NOT terminal 20
  • no_activity status = in_progress AND no activity logged in last 48h 20
  • high_priority_idle priority IN (urgent, high) AND status = pending 15
  • stalled_checklist has checklist items AND none completed AND status = in_progress 10

Follow-up Flow

  1. Handler: validate revision_type (syntactic)

    Accepts: shorter, softer, firmer, translated, explanation. Returns 400 on unknown type.

  2. Use case: check followups_enabled org config (policy)

    Reads org_module_configs[fieldforce][risk_interventions.followups_enabled]. Returns 403 if disabled.

  3. AccessGate.Authorize

    Feature key fieldforce:risk_intervention. Gate denial returns error — no LLM call.

  4. LLM rewrites current draft text

    Narrow, bounded operation — not free-form chat. Transforms the existing recommendation only.

  5. Append revision row

    Saves to ff_risk_intervention_revisions with revision_type and new draft_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.

ColumnTypeNotes
iduuid PK
org_idtext NOT NULLtenant scope
task_iduuid NOT NULLno hard FK in MVP
risk_datedate NOT NULLorg-local date of generation
statustext NOT NULLopen, dismissed, acted_on, expired
risk_scoreint NOT NULLdeterministic additive sum of rule weights
risk_reasonsjsonb NOT NULLarray of rule keys
recommended_actiontext NOT NULLassignee_nudge, supervisor_escalation, reassign
risk_summary_mdtext NOT NULLalways populated; Go-template for heuristic rows, LLM prose for llm rows
draft_texttextnull for heuristic-only rows
generation_modetext NOT NULLpending, llm, heuristic_only
prompt_template_versionintnull for heuristic rows
assigned_team_idstext[] NOT NULLsnapshotted at generation time for supervisor filtering
assignee_user_idstext[] NOT NULLsnapshotted display/filter context
created_attimestamptz NOT NULL
updated_attimestamptz NOT NULL
dismissed_attimestamptzset on dismiss
acted_on_attimestamptzset 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.

ColumnTypeNotes
iduuid PK
intervention_iduuid NOT NULLparent intervention
revision_typetext NOT NULLoriginal, shorter, softer, firmer, translated, explanation
draft_texttext NOT NULL
created_bytext NOT NULLuser ID
created_attimestamptz 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-wide

See all org recommendations. No team filtering applied.

Supervisor

team-scoped

Sees recommendations where snapshotted assigned_team_ids intersects their current teams (ADR-0017 member scan at read time).

Field Team

403

Route returns 403. Panel route is hidden. Not their surface.

Status Transitions

ff_risk_interventions.status state machine — all terminal states are final in MVP
ff_risk_interventions.status state machine — all terminal states are final in MVP

Panel Experience

Inbox list
/app/fieldforce/risk-inbox
Detail view
/app/fieldforce/risk-inbox/:id
SCORE 75 Quarterly Equipment Maintenance — Site B in_progress
assignee_nudge
overdue no_activity

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.

John Doe · Team Alpha · Generated 3 hours ago

Inbox list card — risk score badge, reason chips, recommended action badge, AI summary, and manager actions.

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)DefaultPurpose
risk_interventions.schedule_local_time"08:30"local daily generation time
risk_interventions.timezone"UTC"IANA timezone for schedule
risk_interventions.shortlist_max10max candidates sent to LLM
risk_interventions.retention_days60how long terminal rows are kept
risk_interventions.expire_after_hours48age-based expiry threshold for stale open rows
risk_interventions.followups_enabledtruecontrols 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.go has 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.