Check-ins and verdicts
Once a day the agent sends a check-in: its north-star, KPIs, work counts and a report for each adopted item. The coach answers with health signals and a verdict per item. In short: permanent means keep it, temporary means keep it for the current task only, and pending_removal means remove it after your owner confirms. The rules below are the exact ones the code runs, also served as JSON at https://upskill.coach/api/v1/rules.
Send a check-in
curl -s -X POST 'https://upskill.coach/api/v1/agents/me/checkins' \
-H 'authorization: Bearer <api_key>' \
-H 'content-type: application/json' \
-d '{"north_star":{"value":15,"evidence":"verified","source":"prod DB query"},"kpis":{"paying_accounts":1},"releases":{"shipped":1,"verified_live":1},"work":{"build_actions":3,"distribution_actions":2},"items":[{"item_id":"<id>","used":true,"uses":4,"successes":4,"failures":0,"minutes_saved":20,"helped":"yes","scope":"ongoing"}],"notes":"Shipped pricing page; 3 directory listings."}'MCP: the check_in tool takes the same fields. Re-sending is safe: a second check-in on the same UTC day merges into that day's check-in. Blocks you send replace the earlier ones, blocks you don't resend are kept, nothing is counted twice, and the response says "merged": true. Send "mode":"new" only for a genuinely separate check-in.
Report every adopted item, including unused ones ("used": false). Use "scope":"task" for an item used for one task only. Set an item's owner_feedback to up or down only if your owner actually said so. Leave out any block you can't measure.
Check-in fields
| field | type | required | notes |
|---|---|---|---|
north_star | object | no | Your north-star value right now. Report it every day |
north_star.value | number | yes | |
north_star.evidence | one of self_reported, verified | no | self_reported (default): your own count. verified: read from a system of record (prod DB, Stripe, analytics, live E2E check) and you can link it in source |
north_star.source | string | no | Where the number comes from: a URL, query name, or system. max 500 chars |
kpis | map of number or object | no | Secondary KPI values: number, or {value, evidence, source}. default {} |
releases | object | no | |
releases.shipped | integer | yes | min 0, max 1000000 |
releases.verified_live | integer | yes | How many of those you verified live end to end (real URL, real request) before calling them done. min 0, max 1000000 |
releases.evidence_urls | array of string | no | at most 20 |
releases.evidence | one of self_reported, verified | no | self_reported (default): your own count. verified: read from a system of record (prod DB, Stripe, analytics, live E2E check) and you can link it in source |
releases.source | string | no | Where the number comes from: a URL, query name, or system. max 500 chars |
work | object | no | |
work.build_actions | integer | yes | Features/fixes shipped or built. min 0, max 1000000 |
work.distribution_actions | integer | yes | Listings, posts, launches, outreach sent, directory submissions. min 0, max 1000000 |
work.evidence | one of self_reported, verified | no | self_reported (default): your own count. verified: read from a system of record (prod DB, Stripe, analytics, live E2E check) and you can link it in source |
work.source | string | no | Where the number comes from: a URL, query name, or system. max 500 chars |
owner_blockers | object | no | |
owner_blockers.asked | integer | yes | New asks to your owner since last check-in. min 0, max 1000000 |
owner_blockers.cleared | integer | yes | min 0, max 1000000 |
owner_blockers.open_now | integer | yes | min 0, max 1000000 |
owner_blockers.total_wait_hours | number | no | Sum of hours the cleared asks waited. min 0, max 100000 |
owner_blockers.open_asks | array of object | no | Each ask still open, with its age. at most 20 |
owner_blockers.open_asks[].age_hours | number | yes | How long this ask has been open, in hours. min 0, max 100000 |
owner_blockers.open_asks[].summary | string | no | max 200 chars |
owner_blockers.evidence | one of self_reported, verified | no | self_reported (default): your own count. verified: read from a system of record (prod DB, Stripe, analytics, live E2E check) and you can link it in source |
owner_blockers.source | string | no | Where the number comes from: a URL, query name, or system. max 500 chars |
owner_corrections | object | no | |
owner_corrections.total | integer | yes | min 0, max 1000000 |
owner_corrections.repeated | integer | yes | Corrections your owner had already given you before. min 0, max 1000000 |
owner_corrections.examples | array of string | no | at most 5 |
owner_corrections.evidence | one of self_reported, verified | no | self_reported (default): your own count. verified: read from a system of record (prod DB, Stripe, analytics, live E2E check) and you can link it in source |
owner_corrections.source | string | no | Where the number comes from: a URL, query name, or system. max 500 chars |
interruptions | object | no | |
interruptions.timeouts | integer | yes | min 0, max 1000000 |
interruptions.killed_tasks | integer | yes | Tasks stopped for running too long. min 0, max 1000000 |
interruptions.evidence | one of self_reported, verified | no | self_reported (default): your own count. verified: read from a system of record (prod DB, Stripe, analytics, live E2E check) and you can link it in source |
interruptions.source | string | no | Where the number comes from: a URL, query name, or system. max 500 chars |
claims | object | no | |
claims.made | integer | yes | Claims about users, metrics or results you made to your owner or in public. min 0, max 1000000 |
claims.unsupported | integer | yes | Of those, ones without a linked source. min 0, max 1000000 |
claims.evidence | one of self_reported, verified | no | self_reported (default): your own count. verified: read from a system of record (prod DB, Stripe, analytics, live E2E check) and you can link it in source |
claims.source | string | no | Where the number comes from: a URL, query name, or system. max 500 chars |
incidents | object | no | |
incidents.destructive | integer | yes | Deletes/sends/charges outside policy. min 0, max 1000000 |
incidents.wrong_entity | integer | yes | Wrong account, card, email or identity used. min 0, max 1000000 |
incidents.notes | string | no | max 500 chars |
incidents.evidence | one of self_reported, verified | no | self_reported (default): your own count. verified: read from a system of record (prod DB, Stripe, analytics, live E2E check) and you can link it in source |
incidents.source | string | no | Where the number comes from: a URL, query name, or system. max 500 chars |
deliverable_progress_pct | number | no | 0-100: how far along the named deliverable is. min 0, max 100 |
items | array of object | no | at most 50, default [] |
items[].item_id | string | yes | max 120 chars |
items[].used | boolean | yes | Did you use this item since the last check-in? |
items[].uses | integer | no | min 0, max 1000000 |
items[].successes | integer | no | Uses where it did what you needed. min 0, max 1000000 |
items[].failures | integer | no | min 0, max 1000000 |
items[].minutes_saved | number | no | min 0, max 100000 |
items[].cost_usd | number | no | min 0, max 1000000 |
items[].helped | one of yes, no, unsure | no | |
items[].scope | one of ongoing, task | no | 'task' if you used it only for one specific task |
items[].owner_feedback | one of up, down, none | no | Only if your owner actually said so |
items[].evidence | one of self_reported, verified | no | self_reported (default): your own count. verified: read from a system of record (prod DB, Stripe, analytics, live E2E check) and you can link it in source |
items[].notes | string | no | max 500 chars |
mode | one of auto, new, merge | no | auto (default): a second check-in on the same UTC day merges into that day's check-in (new values replace old ones, nothing is double counted). merge: always merge into your latest check-in. new: force a separate check-in. default "auto" |
tasks | object | no | |
tasks.attempted | integer | no | min 0, max 1000000 |
tasks.succeeded | integer | no | min 0, max 1000000 |
tasks.minutes | number | no | min 0 |
tasks.cost_usd | number | no | min 0 |
owner_feedback | object | no | |
owner_feedback.rating | integer | no | min 1, max 5 |
owner_feedback.comment | string | no | max 500 chars |
notes | string | no | max 2000 chars |
A nested field marked required is only required when its parent is sent.
Item verdicts
| verdict | label | what to do |
|---|---|---|
permanent | Permanent skill | Keep it installed. |
promising | Promising (not proven yet) | Keep using it and report verified values where you can. |
temporary | Temporary (this task only) | Keep it for the current task, then uninstall. |
trial | Trial (collecting evidence) | Keep using it; not enough evidence yet. |
unproven | Unproven | Evidence is mixed; keep measuring. |
pending_removal | Pending removal | Ask your owner to confirm, uninstall, then send {"item_ids":["<id>"],"action":"remove"} to POST /api/v1/agents/me/items. |
How a verdict is decided
At each check-in, for every adopted item, the coach looks only at check-ins since that item was adopted. Rules run in this order and the first match wins:
| rule | verdict | condition |
|---|---|---|
R1_owner_down | pending_removal | Owner gave a thumbs-down on the latest report for this item. |
R2_unused | pending_removal | Adopted but not used in any of the last 3+ check-ins. |
R3_failing | pending_removal | 3+ attempts and success rate under 50%. |
R4_kpi_regressed | pending_removal | Used in 2+ check-ins and its related KPIs got worse by 10%+ vs baseline (and nothing else shows it helping). |
R5_task_scoped | temporary | Most reports say it was used for one specific task (scope=task) and it worked (success rate 50%+). |
R6_proven | permanent | All of: used in 5+ check-ins on 3+ different days; success rate 70%+ (50%+ with an owner thumbs-up) or no failures reported; KPI trend positive (related KPIs, or all KPIs if none are related, better than baseline in their direction); and either at least one verified value since adoption or an owner thumbs-up. |
R7_promising | promising | Working but not proven yet: an owner thumbs-up with usage, or used in 2+ check-ins with success rate 70%+ and a gain (related KPIs up 10%+ vs baseline while in use, a positive KPI trend, 30+ minutes saved, or reported helpful in most check-ins). Becomes permanent only when R6 is fully met. |
R8_one_off | temporary | Used in exactly one check-in, then not again for 2+ check-ins, and that use worked. |
R9_too_early | trial | Fewer than 2 check-ins with usage: not enough evidence yet. |
R10_mixed | unproven | Enough usage, but the evidence is mixed. Keep measuring; it will be marked for removal if it doesn't improve. |
The bar for permanent: used in 5+ check-ins on 3+ different UTC days, success rate 70%+, a positive KPI trend, and at least one verified value or an owner thumbs-up. Each verdict comes with its rule id and a reason that quotes the numbers it used, so the agent can see what is missing.
KPI movement is only linked to an item after 2 or more check-ins where it was used, and it is shown as a correlation, not proof. An item's related KPIs are the north-star and KPIs whose tags overlap the item's tags.
Updating items
POST /api/v1/agents/me/items (MCP: update_items):
| field | type | required | notes |
|---|---|---|---|
item_ids | array of string | yes | at least 1, at most 20 |
action | one of adopt, remove, decline, not_relevant | no | adopt; remove (was adopted, now uninstalled); decline (owner said no); not_relevant (doesn't fit your job; not a judgment on the item's quality). default "adopt" |
reason | string | no | max 300 chars |
decline is for items your owner rejected and not_relevant for items that don't fit your job. Neither is recommended again.
Health
The check-in response has health, health_trend (improving, flat or worsening vs the previous check-in), health_confidence (verified, partly_verified or unverified), signals, item verdicts, verdict_changes and actions. Do what actions says. Health rules run in order; the first match wins. Weekly signals use check-ins from the last 7 days.
| health | rule |
|---|---|
no_signal | No check-in yet, or the last one is more than 48h old. |
at_risk | A risk signal is active: repeated corrections, boundary incidents, unsupported claims, or 2+ interruptions/day in the last 7 days, AND the latest check-in either repeats it or doesn't report that block. Recent check-ins weigh more: if the latest check-in reports that block clean, the older event is shown as warn ('earlier; latest clean') and no longer forces at_risk. |
recovering | There were risk events earlier in the last 7 days, the latest check-in is clean, and the trend vs the previous check-in is improving (north-star better in its direction, or deliverable progress up). |
stalled | Building with no distribution, or the deliverable hasn't moved in 3 check-ins. |
on_track | North-star fresh, releases verified live (when shipping), distribution not neglected, the north-star better than baseline, the declared deliverable moving, and evidence confidence not bad. With 0% verified evidence it's labeled confidence: unverified. |
needs_attention | Anything else. |
Signals
| signal | label | rule |
|---|---|---|
north_star_fresh | North-star reported in the last 24h | good if the latest check-in includes a north-star value and is under 24h old; bad if no north-star is declared or it hasn't been reported for 48h+; warn otherwise. |
definition_stable | North-star definition stable | warn if the definition (or key) changed in the last 7 days; good otherwise. The value says how long the current version has actually existed. Both series stay visible in the report. |
evidence_confidence | Evidence confidence | share of reported values tagged verified (with a source), over all check-ins. good at 50%+; warn under 50%; bad if 0% after 2+ check-ins (warn at the first). When bad, health can't be on_track. |
live_verified_ship_rate | Releases verified live end to end | verified_live / shipped over 7 days. good at 90%+, warn 50-89%, bad under 50%. unknown if nothing shipped. |
distribution_build_ratio | Distribution vs build | distribution_actions / build_actions over 7 days. bad if building with zero distribution; warn under 0.5; good at 0.5+. |
owner_blocker_dwell | Owner-blocker wait time | open asks and their current age (age_hours reported + time since that check-in), plus avg wait of cleared asks. bad if any open ask is older than 24h, avg cleared wait > 72h, or 5+ open; warn if anything is open, avg > 24h; good if nothing is open and waits are short. |
repeat_corrections | Repeated owner corrections | bad if the owner had to repeat any correction in the last 7 days; good if zero. |
interruptions | Timeouts and killed tasks | per day, 7-day average. bad at 2+/day, warn above 0, good at 0. |
unsupported_claims | Claims without evidence | bad if any claim about users/metrics/results had no linked source in the last 7 days. |
boundary_incidents | Destructive / wrong-entity mistakes | bad if any destructive action outside policy or wrong account/card/identity in the last 7 days. |
deliverable_progress | Progress toward the named deliverable | from deliverable_progress_pct, or computed from the deliverable's kpi_key (baseline → target). warn if not reported, at 0%, or only reported once; bad if it hasn't moved across the last 3 check-ins (and isn't 100%); good if it moved. A declared deliverable that isn't moving blocks on_track. |
north_star_trend | North-star vs baseline | direction-adjusted change from baseline to latest: good if better, bad if worse, warn if flat. |