| name | risk |
| description | "Cross-project risk analyzer — identifies open Todoist tasks where follow-through is missing by cross-referencing sent mail, project logs, and calendar. Scores risk by temporal compounding, escalates Todoist priorities for Critical/At Risk items, and sends Pushover alerts. Use daily, when asking 'what have I dropped', or to audit follow-through across projects." |
Risk — Follow-Through Analyzer
What This Does
Scans open Todoist tasks across all (or one) project, cross-references against sent email, project logs, and calendar to determine whether commitments have been actioned. Scores each unactioned item by how long it's been neglected and how serious the gap is. Automatically escalates Todoist priorities for Critical and At Risk items, and sends a Pushover notification.
Runtime Context
At the start of every run, look up the current user's profile to get dynamic values. Use Supabase MCP tools (discover via ToolSearch at runtime) to execute:
SELECT id AS user_id, email, display_name, timezone
FROM user_profiles
WHERE id = auth.uid()
LIMIT 1;
Use the returned values throughout this skill:
user_id — for all DB reads/writes (replaces any hardcoded UUID)
email — for Gmail sent-mail searches (e.g., from:{email})
display_name — for display purposes
timezone — for date calculations
If auth.uid() is not available in your SQL context, retrieve the user ID from the session environment or manifest instead.
Data Storage Rules
- DB is source of truth via Supabase MCP
execute_sql. Discover MCP tools via ToolSearch at runtime — never hardcode tool names.
- Project DB IDs come from the SessionStart manifest (injected into context). Each project entry includes
DB ID, Slug, Todoist ID, and Path.
- User ID for all DB writes: Use
auth.uid() in SQL or the user_id from the Runtime Context lookup above.
- Secrets from Vault: Use
$OUTWORKOS_ROOT/scripts/get-secret.sh <label> to retrieve API tokens at runtime.
Arguments
| Invocation | Behavior |
|---|
/risk | All projects with open tasks + Todoist IDs |
/risk sprint | Single project by name or slug |
/risk all | Explicit all-projects scan |
Matching is case-insensitive against both project name and slug from the SessionStart manifest.
Risk Rubric
Five Risk Signals
| # | Signal | Description | Base Weight |
|---|
| 1 | Commitment breach | Promised something (in sent email or meeting) with no evidence of delivery | 5 |
| 2 | Unanswered inbound | Stakeholder sent a message and no reply exists | 4 |
| 3 | Meeting without follow-through | Meeting occurred but no tasks created or emails sent afterward | 3 |
| 4 | Communication decay | No outbound email to stakeholder while tasks are open | 2 |
| 5 | Stale engagement | Zero activity across all sources for a project with open commitments | 1 |
Temporal Compounding
Risk compounds non-linearly with time. Days are measured from the most recent evidence of activity (sent email, log entry, task completion) for that specific task or stakeholder.
| Tier | Label | Days Since Last Activity | Time Multiplier |
|---|
| 0 | Clear | 0–3 | 1.0 |
| 1 | Watch | 4–5 | 1.5 |
| 2 | At Risk | 6–7 | 3.0 |
| 3 | Critical | 7+ | 5.0 |
Scoring
risk_score = base_weight x time_multiplier
A commitment breach at 8 days = 5 x 5.0 = 25 (critical)
Communication decay at 4 days = 2 x 1.5 = 3 (watch)
Projects are ranked by their highest single risk score. Tasks within a project are ranked by individual score descending.
Per-Project Threshold Overrides
Some projects have different natural cadences. Check the context map for a Risk Cadence field. If present, use those thresholds instead of defaults. If absent, use defaults.
Example override: Risk Cadence: 30/60/90 means Watch at 30 days, At Risk at 60, Critical at 90 (for quarterly-cadence projects like Board Communication).
What Does NOT Get Flagged
- Projects with no open Todoist tasks (dormant by design)
- Tasks in the Waiting section with evidence of a recent handoff email (ball is in their court)
- Tasks in the Defer section (explicitly parked)
- Internal-only tasks with no external stakeholder
- Routine automated notifications or newsletters
- Conversational closure — stakeholder replies that are confirmations ("that works", "sounds good"), delegations to a third party ("@Gina, can you set up..."), or informational with no call to action directed at the user. These do not count as unanswered inbound.
- Scheduling tasks with calendar confirmation — if a task is about scheduling a meeting and a matching calendar event exists within the next 7 days with the same stakeholder, treat as Clear regardless of email thread state.
Phase 0: Load State + Todoist Sync
0a. Resolve Target Projects
If an argument was provided:
- Normalize to lowercase
- Match against SessionStart manifest by project name (case-insensitive) or slug
- If found, scope to that single project
- If no match, list available projects with Todoist IDs
If no argument or all:
- Get all projects from the manifest that have a
Todoist ID
- These are the candidates for Phase 1
0b. Todoist Sync + Label Reset (Shared Cache)
Reuse the shared Todoist sync cache. Read the script in references/api-patterns.md.
- Check
/tmp/todoist-sync-cache.json — if fresh (< 5 min), reuse /tmp/scan-items.json
- If stale, do a full sync: items, sections, projects, labels
- Save to
/tmp/risk-items.json, /tmp/risk-sections.json
- Create
risk label if it doesn't exist (color: red)
- Reset
risk label — clear from all tasks so it reflects only this run's output (same pattern as /scan resets the scan label)
0c. Filter to Active Projects
From the synced items, identify which candidate projects actually have open tasks:
- Filter items by
project_id matching each candidate's Todoist ID
- Exclude tasks in Defer sections (look up section names from sections data)
- A project qualifies for a risk worker if it has >= 1 non-deferred open task
Output: List of qualifying projects with their task counts.
Phase 1: Spawn Per-Project Risk Workers (Parallel)
For each qualifying project, spawn a worker agent. Run all workers in parallel.
Read the detailed worker prompt in references/worker-prompt.md.
Worker Agent Settings
| Setting | Value |
|---|
subagent_type | general-purpose |
name | risk-worker-{project_slug} |
What Each Worker Does
-
Load project context — context map from local file at $OUTWORKOS_PARENT/{ProjectName}/context-map.md. Extract stakeholder names, emails, person-to-email mappings.
-
Get open tasks — from /tmp/risk-items.json, filter to this project's Todoist ID. Exclude Defer section tasks.
-
Search sent mail — for each person in the context map, search all their known email addresses. Use the email from the Runtime Context lookup (user_profiles) as the sender:
from:{user_email} (to:{email1} OR to:{email2}) newer_than:30d
Fetch full content for each result. Build evidence pool.
-
Query project logs — via Supabase MCP, query log_entries for this project:
SELECT logged_at, summary, details FROM log_entries
WHERE project_id = '{project_db_id}'
AND logged_at > now() - interval '30 days'
ORDER BY logged_at DESC LIMIT 20;
-
Check calendar — search for meetings with stakeholder in the past 14 days and next 7 days.
-
Score each task — for every open task, evaluate all 5 risk signals. Compute days_since_last_activity by finding the most recent evidence across:
- Sent email to the relevant person
- Log entry mentioning the task topic
- Calendar meeting with the stakeholder
- Task modification date
-
Return structured JSON — see references/worker-prompt.md for the full schema.
Collecting Worker Results
- Wait for all workers to return
- If a worker times out (>90s) or returns invalid JSON: record
{"project": "NAME", "error": "timeout"} and continue
- Collect all results into
/tmp/risk-results.json
Phase 2a: Auto-Close Resolved Tasks
Before scoring or escalating, collect all tasks from worker results where auto_close: true (tier "resolved"). These are tasks where the worker found evidence the action was already taken but the Todoist checkbox was never ticked.
Build Auto-Close Batch
close_ids = []
for result in all_worker_results:
for risk in result.get('risks', []):
if risk.get('tier') == 'resolved':
close_ids.append(risk['task_id'])
Execute via Todoist Sync API
import uuid
commands = []
for task_id in close_ids:
commands.append({
'type': 'item_close',
'uuid': str(uuid.uuid4()),
'args': {'id': task_id}
})
Send as a single Sync API batch (same pattern as Phase 2b). If the list is empty, skip.
Exclude resolved tasks from all subsequent phases — they should not be scored, escalated, labeled, or included in the notification. They appear only in the "Auto-Closed" section of the output (Phase 4).
Phase 2: Synthesize + Rank
Merge all worker results, excluding resolved/auto-closed tasks. For each project:
- Take the highest-scored risk item as the project's headline risk
- Count items per tier (Critical / At Risk / Watch / Clear)
Rank projects by highest single risk score descending.
Cross-project pattern detection:
- If 3+ projects have Critical items, note "systemic follow-through gap"
- If a single person appears in risk items across multiple projects, note "multi-project stakeholder at risk: {name}"
Phase 2b: Escalate Todoist Priorities
Automatically escalate task priorities for Critical and At Risk items. Only escalates, never downgrades. If a task is already at or above the target priority, skip it.
Escalation Rules
| Risk Tier | Target Priority | Todoist Value | Condition |
|---|
| Critical (7+ days) | P1 (Urgent) | priority: 4 | Only if current priority < 4 |
| At Risk (6-7 days) | P2 (High) | priority: 3 | Only if current priority < 3 |
| Watch | No change | — | — |
| Clear | No change | — | — |
Build Batch Update
Collect all tasks that need escalation from the merged worker results:
escalations = []
for result in all_worker_results:
for risk in result.get('risks', []):
if risk['tier'] == 'critical' and risk.get('priority', 4) < 4:
escalations.append({'task_id': risk['task_id'], 'old_priority': risk['priority'], 'new_priority': 4, 'task_content': risk['task_content'], 'project': result['project_name'], 'tier': 'critical'})
elif risk['tier'] == 'at_risk' and risk.get('priority', 3) < 3:
escalations.append({'task_id': risk['task_id'], 'old_priority': risk['priority'], 'new_priority': 3, 'task_content': risk['task_content'], 'project': result['project_name'], 'tier': 'at_risk'})
If escalations is empty, skip the API call.
Apply via Todoist Sync API
Read the batch update pattern in references/api-patterns.md (Phase 2b section).
Build item_update commands for each escalation and send as a single Sync API batch call. Record which tasks were escalated for Phase 4 output.
Apply risk Label
In the same Sync API batch call, apply the risk label to ALL Critical and At Risk tasks (not just those whose priority changed). This includes tasks already at or above the target priority.
For each Critical or At Risk task, add risk to its existing labels array:
labels = list(set(task['labels'] + ['risk']))
This makes @risk a live Todoist filter showing everything that needs attention right now. The label is ephemeral — reset at the start of each run (Phase 0b), so it always reflects the latest assessment.
Phase 3: Pushover Notification
If ANY items scored At Risk (tier 2) or Critical (tier 3), send a Pushover notification.
Build Notification
Title: Risk Alert: {count} items need attention
Message: List the top 3 highest-scored items (max 500 chars for Pushover):
CRITICAL: Sprint — Reply to Robert re: pipeline (8d)
CRITICAL: Scale AI — Send contract to Alex (12d)
AT RISK: AICON — Follow up with Colin (7d)
{count - 3} more items. Run /risk for full report.
Priority: If any Critical items exist, use priority 1 (high — bypasses quiet hours). Otherwise use priority 0 (normal).
URL: Link to the @risk label view in Todoist so tapping the notification opens all flagged tasks:
- Label view URL:
https://app.todoist.com/app/label/risk
- Use the
url and url_title Pushover parameters
Send Notification
_ROOT="${OUTWORKOS_ROOT:?OUTWORKOS_ROOT not set}"
PUSHOVER_API_TOKEN=$("$_ROOT/scripts/get-secret.sh" pushover_api_token)
PUSHOVER_USER_KEY=$("$_ROOT/scripts/get-secret.sh" pushover_user_key)
curl -s -o /dev/null -w "%{http_code}" \
--form-string "token=$PUSHOVER_API_TOKEN" \
--form-string "user=$PUSHOVER_USER_KEY" \
--form-string "title=Risk Alert: {count} items" \
--form-string "message={message}" \
--form-string "priority=$PRIORITY" \
--form-string "sound=siren" \
--form-string "url=https://app.todoist.com/app/label/risk" \
--form-string "url_title=View all risk items in Todoist" \
https://api.pushover.net/1/messages.json
If NO items are At Risk or Critical, skip the notification entirely. Don't send "all clear" — silence means no risk.
Phase 4: Present Results
## Risk Report — {date}
### Auto-Closed ({count})
Tasks completed in Todoist — evidence confirms the action was already taken.
| Task | Project | Evidence |
|------|---------|----------|
| Find and review Gina's renewal agreement | RBCx | Sent "Just signed the 2026 partnership agreement" on Mar 1. Gina confirmed receipt Mar 2. |
| Reply to Lorie — monthly check-ins | RBCx | Lorie delegated to Gina ("@Gina, do you want to set up a recurring booking?"). Conversational closure — no action needed. |
### Critical ({count})
Items 7+ days without evidence of follow-through.
| Score | Task | Project | Signal | Days | Evidence Gap |
|-------|------|---------|--------|------|--------------|
| 25 | Reply to Robert — pipeline update | Sprint | Commitment breach | 12d | Wrote "I'll send the pipeline by Friday" on Feb 23. No sent email with "pipeline" to Robert since. |
| 20 | Send contract to Alex | Scale AI | Commitment breach | 8d | Meeting action item from Feb 27. No sent email to alex@scale.ai since. |
### At Risk ({count})
Items 6-7 days without activity.
| Score | Task | Project | Signal | Days | Evidence Gap |
### Watch ({count})
Items 4-5 days — emerging gaps.
| Score | Task | Project | Signal | Days |
### Clear ({count})
{count} tasks across {project_count} projects have recent activity. No action needed.
### Priority Escalated ({count})
Tasks whose Todoist priority was automatically raised.
| Task | Project | Previous | New | Reason |
|------|---------|----------|-----|--------|
| Reply to Robert — pipeline update | Sprint | P3 | P1 | Critical: 12d commitment breach |
| Follow up with Colin | AICON | P3 | P2 | At Risk: 7d unanswered inbound |
### Cross-Project Patterns
- {any systemic patterns detected}
---
Auto-closed: {count} tasks.
Pushover notification sent: {count} items alerted.
Priority escalated: {count} tasks.
Phase 5: Update State
INSERT INTO skill_state (user_id, skill_name, state_key, state_value)
VALUES (auth.uid(), 'risk', 'last_run', '{
"last_run": "{ISO-8601 timestamp}",
"projects_scanned": N,
"tasks_assessed": N,
"critical_count": N,
"at_risk_count": N,
"watch_count": N,
"clear_count": N,
"priorities_escalated": N,
"auto_closed": N,
"notification_sent": true/false
}')
ON CONFLICT (user_id, skill_name, state_key)
DO UPDATE SET state_value = EXCLUDED.state_value, updated_at = now();
Edge Cases
- No context map found for a project: Skip project, note in output. Suggest running
/context-map.
- No open tasks across any project: Report "No open tasks found" and exit. No notification.
- Todoist token expired (401/403): Stop and tell user to update via
$OUTWORKOS_ROOT/scripts/set-secret.sh todoist_api_token <new_token>
- Gmail OAuth expired: Direct user to re-auth via
$OUTWORKOS_ROOT/scripts/google-auth.sh
- Pushover credentials missing: Skip notification, note in output. Suggest setting up via Vault.
- Worker timeout: Record error, continue with other workers. Include note in output.
- Tasks in Waiting section: Only flag if NO outbound email was sent handing off. If a handoff email exists, task is legitimately waiting — skip it.
- All items Clear: No notification, brief "All clear" output.
Compatibility
- Shared Todoist cache with
/scan and /whats-next at /tmp/todoist-sync-cache.json (5-min TTL)
- Priority escalation — escalates priorities for Critical (→P1) and At Risk (→P2) items. Never downgrades.
risk label — ephemeral, reset each run. Applied to all Critical and At Risk tasks. Filter @risk in Todoist = "what's at risk right now." Does not touch scan or gap labels.
- No task creation — run
/whats-next or /scan to create tasks from gaps.
- Log entries — reads
log_entries table but does not write to it. Use /log to record sessions.