← Metrics Desk / API
Tokens

Drive Metrics Desk from your own code

Everything the web page does is available over HTTP. Send a product's period-by-period metrics with the facts the browser computes for them and get the same metrics review back: an overall call (on_track, mixed, off_track), one scorecard reading per metric, wins, concerns, anomalies, typed actions and the metric to drill next. Or send a segment breakdown of one metric and get the drill-down: a verdict (rate, mix, mixed, volume, inconclusive), the drivers with their exact contributions, offsets, hypotheses with tests and the next queries. The natural use is a scheduled job: every Monday a script exports last week's metrics, files the review in the team channel, and drills into whatever went red.

The model never does the arithmetic. Change (in points for a percentage), attainment, status, trend, streak, z-score, lifecycle coverage, the rate / mix / interaction split and whether the segments add back to the scorecard are all worked out by metricskit.js, the same file the web page loads, and sent as pack, a JSON string. See building the body.

Two lanes: the task field

Every request names its lane in task, and one system prompt routes on it.

taskwhat it does
reviewThe metrics review (metrics-review): overall, headline, summary, a scorecard entry per metric (ref, status, read, why), wins, concerns, one anomalies entry per metric at |z| >= 2 (persistence, check), 2-5 actions typed investigate, experiment, invest or alert with a success_metric, caveats, drill_next and one prescan_responses entry per prescan flag.
drillThe drill-down (analyze): metric_ref, verdict, headline, decomposition, 1-4 drivers (ref, numeric contribution copied from the pack, explanation, confidence), offsets, 2-4 hypotheses with a test, validation, next_queries, recommendation, summary and the prescan responses.

The lanes chain: a review's drill_next names the metric to drill, and its overall call and reading of that metric can travel as the drill's optional review field. See handing the review on. A missing or unknown task is answered as the closest lane and the reply's lane says which. Always send task.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses the same envelope, so one helper covers the whole API:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest endpoint takes {"slug":"metrics-desk"} in its body), so no slug header is needed afterwards. Send it as Authorization: Bearer ….

The input object IS the request body. There is no {"input": …} wrapper. A wrapped body returns a 200 with an unknown field 'input' warning, and the model never sees your metrics.

Error codes

codestatuswhat to do
unauthorized401The token is missing, malformed or expired. Get a new one from the token page.
payment_required402The balance is below min_credits. Call /estimate first and top up.
forbidden403The token is valid but not for this app, or a guest token tried a metered run.
not_found404Unknown job id, unknown collection, or the app slug does not exist.
conflict409The same Idempotency-Key was replayed with a different body. Change the key or send the original input.
validation_error422A field is the wrong type. Every field is a string: pack and review must be JSON-encoded strings, not objects.
rate_limited429Too many requests. Back off and retry; do not tight-loop.
internal5xxA server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice.

1. Get a token

The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. A guest token can call /me and /estimate; both lanes are metered, so a run needs a personal token.

# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
#   https://metrics-desk.skillsafe.ai/tokens.html
#   export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead. A guest token is enough
# for /me and /estimate; both lanes need a personal token
# from signing in.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
  -H "Content-Type: application/json" -d '{"slug":"metrics-desk"}'
# {"ok":true,"data":{"token":"…","subject_type":"guest"}}

2. A tiny client

One helper that sends the token, unwraps data and raises on ok: false.

# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="metrics-desk"
TOKEN="$SKILLSAFE_TOKEN"   # from https://metrics-desk.skillsafe.ai/tokens.html

call() {                  # call <path> [json-body]
  if [ -n "$2" ]; then
    curl -sS -X POST "$BASE/$1" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d "$2"
  else
    curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN"
  fi
}

3. Check the session and the balance

call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}

4. Price the run (free)

/estimate returns the model binding and the credits a run would reserve. It creates no job and charges nothing. It also does not validate the body, so check the shape yourself: an object whose every value is a string, task one of the two lanes, and pack a JSON string. Re-estimate per lane - the two lanes reserve different amounts.

# body.json is the input object itself - no {"input": ...} wrapper. Build it with
# make-body.js below. estimate does not validate it, so check the shape first:
python3 -c 'import json;b=json.load(open("body.json"));assert isinstance(b,dict) and b.get("task") in ("review","drill") and all(isinstance(b.get(k),str) and b[k].strip() for k in ("product","cadence","pack")) and isinstance(json.loads(b["pack"]),dict)'
INPUT=$(cat body.json)
LANE=$(printf '%s' "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["task"])')

call estimate "$INPUT"
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
#   "markup_bps":1000,"hold_credits":...,"min_credits":...,"sponsor_enabled":false,
#   "input_checked":true,"warnings":[]}}
#
# estimate creates no job and charges nothing. hold_credits is RESERVED, not the
# price; charged_credits after the run is usually far lower.

Building the body

Do not hand-assemble pack. Load metricskit.js (it runs unchanged in Node) and let it read your table exactly as the page does:

// make-body.js - build the run body with the SAME engine the web page uses.
// Save metricskit.js from https://metrics-desk.skillsafe.ai/metricskit.js next to this file.
const fs = require("fs");
const K = require("./metricskit.js");

const A = K.analyze({
  product: "Brindlewick - team analytics SaaS, self-serve trial",
  cadence: "weekly",
  metrics: fs.readFileSync("metrics.csv", "utf8"),      // one row per metric, one column per period
  segments: fs.existsSync("segments.csv") ? fs.readFileSync("segments.csv", "utf8") : "",
  drill: "Activation rate",                              // the metric the breakdown is for
  notes: "W38: paid social campaign launched, budget tripled"
});
const lane = process.argv[2] || "review";                // "review" or "drill"
const body = K.buildInput(lane, A, { question: "Is onboarding broken?" });
if (!body) throw new Error(lane === "drill" ? "the drill lane needs a segment breakdown and a chosen metric" : "no metrics read");
fs.writeFileSync("body.json", JSON.stringify(K.mustBeObject(body)));
console.log(lane, A.metrics.length, "metrics,", A.flags.length + A.drill.flags.length, "flags");

The input fields, every one a string:

fieldrequiredwhat it holds
taskyesreview or drill.
productyesWhat the product is, in a few words.
cadenceyesweekly, monthly or quarterly.
packyesJSON string: periods, counts, metrics (M1..Mn, each with values and the browser's facts), prescan_flags (P1..Pn), and in the drill lane drill (mode, totals, rate / mix / interaction, verdict_hint, segments S1..Sn with contributions). The drill lane sends only the flags about the drilled metric plus the breakdown's own.
notesnoContext lines, [N1] ...: launches, campaigns, incidents.
questionnoWhat you want to know.
reviewno (drill only)JSON string of the review's overall call and reading of the drilled metric.
retry_notenoOnly on a reformat retry.

Worked inputs

The Brindlewick example from the page, shortened (the real pack is a JSON string, shown expanded here).

review:

{
 "task": "review",
 "product": "Brindlewick - team analytics SaaS, self-serve trial",
 "cadence": "weekly",
 "pack": "<JSON string of:> {\n \"cadence\": \"weekly\",\n \"periods\": [\n  \"W31\",\n  \"W32\",\n  \"W33\",\n  \"W34\",\n  \"W35\",\n  \"W36\",\n  \"W37\",\n  \"W38\"\n ],\n \"counts\": {\n  \"green\": 4,\n  \"amber\": 1,\n  \"red\": 1,\n  \"grey\": 0\n },\n \"metrics\": [\n  {\n   \"id\": \"M1\",\n   \"name\": \"Weekly signups\",\n   \"stage\": \"acquisition\",\n   \"unit\": \"\",\n   \"better\": \"up\",\n   \"better_inferred\": false,\n   \"target\": 2400,\n   \"periods\": [\n    \"W31\",\n    \"W32\",\n    \"W33\",\n    \"W34\",\n    \"W35\",\n    \"W36\",\n    \"W37\",\n    \"W38\"\n   ],\n   \"values\": [\n    2010,\n    2080,\n    2150,\n    2120,\n    2210,\n    2260,\n    2340,\n    3120\n   ],\n   \"latest\": 3120,\n   \"latest_period\": \"W38\",\n   \"prior\": 2340,\n   \"prior_period\": \"W37\",\n   \"change\": 780,\n   \"change_pct\": 33.33,\n   \"change_pp\": null,\n   \"vs_target\": 720,\n   \"attainment_pct\": 130,\n   \"miss_pct\": 0,\n   \"status\": \"green\",\n   \"status_basis\": \"target\",\n   \"trend_slope_pct_per_period\": 4.94,\n   \"streak\": 4,\n   \"streak_dir\": \"up\",\n   \"z_score\": 8.51,\n   \"baseline_periods\": 7,\n   \"baseline_mean\": 2167.1429\n  },\n  {\n   \"id\": \"M2\",\n   \"name\": \"Activation rate\",\n   \"stage\": \"activation\",\n   \"unit\": \"%\",\n   \"better\": \"up\",\n   \"better_inferred\": false,\n   \"target\": 42,\n   \"periods\": [\n    \"W31\",\n    \"W32\",\n    \"W33\",\n    \"W34\",\n    \"W35\",\n    \"W36\",\n    \"W37\",\n    \"W38\"\n   ],\n   \"values\": [\n    41.2,\n    41.8,\n    42.3,\n    41.9,\n    42.6,\n    42.1,\n    42.4,\n    37.1\n   ],\n   \"latest\": 37.1,\n   \"latest_period\": \"W38\",\n   \"prior\": 42.4,\n   \"prior_period\": \"W37\",\n   \"change\": -5.3,\n   \"change_pct\": -12.5,\n   \"change_pp\": -5.3,\n   \"vs_target\": -4.9,\n   \"attainment_pct\": 88.3,\n   \"miss_pct\": 11.67,\n   \"status\": \"red\",\n   \"status_basis\": \"target\",\n   \"trend_slope_pct_per_period\": -0.74,\n   \"streak\": 1,\n   \"streak_dir\": \"down\",\n   \"z_score\": -10.63,\n   \"baseline_periods\": 7,\n   \"baseline_mean\": 42.0429\n  },\n  {\n   \"...\": \"4 more metrics\"\n  }\n ],\n \"prescan_flags\": [\n  {\n   \"id\": \"P1\",\n   \"severity\": \"high\",\n   \"category\": \"target\",\n   \"ref\": \"M2\",\n   \"message\": \"Activation rate is 4.9 pts below its target of 42% (11.67% miss, W38).\"\n  },\n  {\n   \"id\": \"P2\",\n   \"severity\": \"high\",\n   \"category\": \"anomaly\",\n   \"ref\": \"M2\",\n   \"message\": \"Activation rate is 10.63 standard deviations below its 7-period baseline (42.04%).\"\n  },\n  {\n   \"...\": \"4 more flags\"\n  }\n ]\n}",
 "notes": "[N1] W38: paid social campaign 'Dashboards in 5 minutes' launched Monday, budget tripled\n[N2] W38: onboarding checklist copy change shipped to 50% of new workspaces\n[N3] W37: partner webinar series paused for the summer",
 "question": "Activation dropped five points in one week. Is onboarding broken, or is something else going on?"
}

drill:

{
 "task": "drill",
 "product": "Brindlewick - team analytics SaaS, self-serve trial",
 "cadence": "weekly",
 "pack": "<JSON string of:> {\n \"cadence\": \"weekly\",\n \"periods\": [\n  \"W31\",\n  \"W32\",\n  \"W33\",\n  \"W34\",\n  \"W35\",\n  \"W36\",\n  \"W37\",\n  \"W38\"\n ],\n \"counts\": {\n  \"green\": 4,\n  \"amber\": 1,\n  \"red\": 1,\n  \"grey\": 0\n },\n \"metrics\": [\n  {\n   \"id\": \"M1\",\n   \"name\": \"Weekly signups\",\n   \"stage\": \"acquisition\",\n   \"unit\": \"\",\n   \"better\": \"up\",\n   \"better_inferred\": false,\n   \"target\": 2400,\n   \"periods\": [\n    \"W31\",\n    \"W32\",\n    \"W33\",\n    \"W34\",\n    \"W35\",\n    \"W36\",\n    \"W37\",\n    \"W38\"\n   ],\n   \"values\": [\n    2010,\n    2080,\n    2150,\n    2120,\n    2210,\n    2260,\n    2340,\n    3120\n   ],\n   \"latest\": 3120,\n   \"latest_period\": \"W38\",\n   \"prior\": 2340,\n   \"prior_period\": \"W37\",\n   \"change\": 780,\n   \"change_pct\": 33.33,\n   \"change_pp\": null,\n   \"vs_target\": 720,\n   \"attainment_pct\": 130,\n   \"miss_pct\": 0,\n   \"status\": \"green\",\n   \"status_basis\": \"target\",\n   \"trend_slope_pct_per_period\": 4.94,\n   \"streak\": 4,\n   \"streak_dir\": \"up\",\n   \"z_score\": 8.51,\n   \"baseline_periods\": 7,\n   \"baseline_mean\": 2167.1429\n  },\n  {\n   \"id\": \"M2\",\n   \"name\": \"Activation rate\",\n   \"stage\": \"activation\",\n   \"unit\": \"%\",\n   \"better\": \"up\",\n   \"better_inferred\": false,\n   \"target\": 42,\n   \"periods\": [\n    \"W31\",\n    \"W32\",\n    \"W33\",\n    \"W34\",\n    \"W35\",\n    \"W36\",\n    \"W37\",\n    \"W38\"\n   ],\n   \"values\": [\n    41.2,\n    41.8,\n    42.3,\n    41.9,\n    42.6,\n    42.1,\n    42.4,\n    37.1\n   ],\n   \"latest\": 37.1,\n   \"latest_period\": \"W38\",\n   \"prior\": 42.4,\n   \"prior_period\": \"W37\",\n   \"change\": -5.3,\n   \"change_pct\": -12.5,\n   \"change_pp\": -5.3,\n   \"vs_target\": -4.9,\n   \"attainment_pct\": 88.3,\n   \"miss_pct\": 11.67,\n   \"status\": \"red\",\n   \"status_basis\": \"target\",\n   \"trend_slope_pct_per_period\": -0.74,\n   \"streak\": 1,\n   \"streak_dir\": \"down\",\n   \"z_score\": -10.63,\n   \"baseline_periods\": 7,\n   \"baseline_mean\": 42.0429\n  },\n  {\n   \"...\": \"4 more metrics\"\n  }\n ],\n \"drill\": {\n  \"metric\": \"M2\",\n  \"metric_name\": \"Activation rate\",\n  \"mode\": \"rate\",\n  \"headers\": {\n   \"prior\": \"W37 activation\",\n   \"current\": \"W38 activation\",\n   \"prior_base\": \"W37 signups\",\n   \"current_base\": \"W38 signups\"\n  },\n  \"totals\": {\n   \"prior\": 42.5214,\n   \"current\": 37.1423,\n   \"delta\": -5.3791,\n   \"prior_base\": 2340,\n   \"current_base\": 3120,\n   \"base_change_pct\": 33.33\n  },\n  \"rate_effect\": -0.3085,\n  \"mix_effect\": -4.8851,\n  \"interaction\": -0.1854,\n  \"dominant\": \"mix\",\n  \"verdict_hint\": \"mix\",\n  \"top_segment\": \"Paid social\",\n  \"top_share_pct\": 82,\n  \"concentrated\": true,\n  \"reconciles\": true,\n  \"folded_segments\": 0,\n  \"segments\": [\n   {\n    \"name\": \"Organic\",\n    \"prior\": 44.5,\n    \"current\": 44.2,\n    \"prior_base\": 1400,\n    \"current_base\": 1450,\n    \"prior_share_pct\": 59.83,\n    \"current_share_pct\": 46.47,\n    \"delta\": -0.3,\n    \"rate_effect\": -0.1795,\n    \"mix_effect\": -0.2642,\n    \"interaction\": 0.0401,\n    \"contribution\": -0.4037,\n    \"share_of_change_pct\": 7.5,\n    \"id\": \"S1\"\n   },\n   {\n    \"name\": \"Referral\",\n    \"prior\": 52,\n    \"current\": 51.5,\n    \"prior_base\": 380,\n    \"current_base\": 400,\n    \"prior_share_pct\": 16.24,\n    \"current_share_pct\": 12.82,\n    \"delta\": -0.5,\n    \"rate_effect\": -0.0812,\n    \"mix_effect\": -0.3241,\n    \"interaction\": 0.0171,\n    \"contribution\": -0.3882,\n    \"share_of_change_pct\": 7.2,\n    \"id\": \"S2\"\n   },\n   {\n    \"...\": \"2 more segments\"\n   }\n  ]\n },\n \"prescan_flags\": [\n  {\n   \"id\": \"P1\",\n   \"severity\": \"high\",\n   \"category\": \"target\",\n   \"ref\": \"M2\",\n   \"message\": \"Activation rate is 4.9 pts below its target of 42% (11.67% miss, W38).\"\n  },\n  {\n   \"id\": \"P2\",\n   \"severity\": \"high\",\n   \"category\": \"anomaly\",\n   \"ref\": \"M2\",\n   \"message\": \"Activation rate is 10.63 standard deviations below its 7-period baseline (42.04%).\"\n  },\n  {\n   \"...\": \"3 more flags\"\n  }\n ]\n}",
 "notes": "[N1] W38: paid social campaign 'Dashboards in 5 minutes' launched Monday, budget tripled\n[N2] W38: onboarding checklist copy change shipped to 50% of new workspaces\n[N3] W37: partner webinar series paused for the summer",
 "question": "Activation dropped five points in one week. Is onboarding broken, or is something else going on?"
}

5. Run it, then poll

POST /run returns a job_id; poll GET /jobs/{id} until it is terminal. Send an Idempotency-Key built from the lane and the input so a retried request returns the same job instead of billing a second run.

# Always send an Idempotency-Key derived from the lane and the input. A retried
# request with the same key returns the SAME job instead of billing a second run.
KEY="metrics-desk:$LANE:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"

JOB=$(curl -sS -X POST "$BASE/run" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')

while :; do
  OUT=$(call "jobs/$JOB")
  STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
  [ "$STATUS" = "succeeded" ] && break
  [ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
  sleep 2
done

# {"ok":true,"data":{"job_id":"job_...","status":"succeeded",
#   "output":{"output":"{\"lane\":\"review\",\"headline\":\"Activation fell 5.3 points on mix, not onboarding ...\", ...}"},
#   "charged_credits":...,"truncated":false}}
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])' > reply.json

6. Or stream it

# Server-sent events. `delta` events carry chunks of the reply; `done` carries the
# status, charged_credits and the truncated flag.
curl -N -X POST "$BASE/run-stream" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -H "Accept: text/event-stream" \
  -d "$INPUT"

# event: job    {"job_id":"job_..."}
# event: delta  {"text":"{\"lane\":\"review\",\"headline\":\"Activation fell 5.3 points"}
# event: done   {"status":"succeeded","charged_credits":...,"truncated":false}

7. Parse the reply

The reply is one JSON object in data.output.output. Strip anything outside the outermost braces.

# reply.json holds data.output.output from step 5. Strip any fence, keep the object:
python3 - <<'EOF'
import json
t = open("reply.json").read()
r = json.loads(t[t.index("{"):t.rindex("}") + 1])
print(r.get("lane"), "-", r["headline"])
if r.get("lane") == "drill":
    print("verdict:", r["verdict"], "metric:", r["metric_ref"])
    for d in r["drivers"]:
        print(d["ref"], d["contribution"], d["confidence"], "|", d["explanation"])
else:
    print("overall:", r["overall"])
    for s in r["scorecard"]:
        print(s["ref"], s["status"], "|", s["read"])
    for a in r["actions"]:
        print("[" + a["type"] + "]", a["action"], "->", a["ref"])
EOF

Invariants worth asserting

Handing the review on

To drill from a review, pull the breakdown the review's drill_next.breakdown asks for, build the drill body with drill set to that metric's name, and add review: JSON.stringify({metric, metric_name, overall, headline, reading, why, breakdown}) - exactly what the page's "Drill into this metric" button sends.

The output contract

review

{"lane":"review","overall":"on_track|mixed|off_track","headline":"...","summary":"...",
 "scorecard":[{"ref":"M1","status":"green|amber|red|grey","read":"...","why":"..."}],
 "wins":[{"ref":"M1","text":"..."}],"concerns":[{"ref":"M2","text":"..."}],
 "anomalies":[{"ref":"M2","explanation":"...","persistence":"one_off|sustained|unclear","check":"..."}],
 "actions":[{"type":"investigate|experiment|invest|alert","action":"...","ref":"M2","success_metric":"..."}],
 "caveats":["..."],"drill_next":{"ref":"M2","why":"...","breakdown":"..."},
 "prescan_responses":[{"id":"P1","status":"confirmed|dismissed","reason":"..."}]}

drill

{"lane":"drill","metric_ref":"M2","verdict":"rate|mix|mixed|volume|inconclusive","headline":"...",
 "decomposition":"...","drivers":[{"ref":"S3","contribution":-4.8621,"explanation":"...","confidence":"high|medium|low"}],
 "offsets":[{"ref":"S4","text":"..."}],"hypotheses":[{"hypothesis":"...","evidence":"S3,N1","test":"..."}],
 "validation":["..."],"next_queries":["..."],"recommendation":"...","summary":"...",
 "prescan_responses":[{"id":"P6","status":"confirmed|dismissed","reason":"..."}]}

Worked replies

Real model replies for the Brindlewick inputs above (lists shortened).

{
 "lane": "review",
 "overall": "mixed",
 "headline": "Activation rate fell 5.3 points to 37.1% in W38, the same week the tripled 'Dashboards in 5 minutes' paid social campaign lifted signups 33.33% to 3120, which points to a change in who signed up rather than broken onboarding.",
 "summary": "Brindlewick grew its top of funnel hard in W38 (signups 3120, 130% of target) while activation dropped to 37.1% (red, 4.9 points under the 42% target) and week-1 retention slipped to 57.6% (amber); downstream trial-to-paid, support load and page speed all held on target. The activation drop is a one-week break from a flat 41.2-42.6% range and coincides with the paid social launch [N1], so the likeliest reading is a lower-intent cohort diluting the rate, not an onboarding failure. The one onboarding change this week [N2] went to only 50% of new workspaces, so comparing its two arms is the fastest way to rule it in or out.",
 "scorecard": [
  {
   "ref": "M1",
   "status": "green",
   "read": "Signups jumped 780 (+33.33%) to 3120, 720 above the 2400 target (130% attainment) and 8.51 standard deviations above the 2167.14 baseline; this sits on top of a 4-week up streak and a 4.94% per week slope.",
   "why": "Consistent with the W38 paid social campaign whose budget tripled [N1]; the prior steady climb predates it."
  },
  {
   "ref": "M2",
   "status": "red",
   "read": "Activation fell 5.3 points (-12.5%) from 42.4% to 37.1%, 4.9 points below the 42% target (11.67% miss) and 10.63 standard deviations below its 42.04% baseline; the previous seven weeks never left 41.2-42.6%.",
   "why": "Likely a mix shift from a surge of paid social signups [N1] that typically activate at a lower rate, with the 50% onboarding copy test [N2] as a secondary suspect; the drill-down on channel will separate the two."
  },
  "... 4 more"
 ],
 "wins": [
  {
   "ref": "M1",
   "text": "Signups at 3120 are 130% of target and the fourth straight up week; the underlying slope of 4.94% per week was positive even before the campaign."
  },
  {
   "ref": "M4",
   "text": "Trial-to-paid held at 12.2%, above its 12% target, despite the top-of-funnel disruption."
  },
  "... 1 more"
 ],
 "concerns": [
  {
   "ref": "M2",
   "text": "Activation at 37.1% is 4.9 points under target and a -10.63 z break; if it is mix, the paid social spend is buying signups that do not activate and the effective cost per activated workspace is much worse than cost per signup suggests."
  },
  {
   "ref": "M3",
   "text": "Week-1 retention is down two weeks running to 57.6%; if the W38 cohort is lower intent this will likely fall further as that cohort matures."
  },
  "... 1 more"
 ],
 "anomalies": [
  {
   "ref": "M1",
   "explanation": "An 8.51 z jump in signups the same week the paid social campaign launched with tripled budget [N1]; consistent with real paid traffic, but bot or duplicate signups from paid social would look the same.",
   "persistence": "unclear",
   "check": "Split W38 signups by acquisition channel and check for duplicate emails, disposable domains and signup tracking or attribution tag changes around the campaign launch."
  },
  {
   "ref": "M2",
   "explanation": "A -10.63 z drop concentrated in the campaign week; consistent with a mix shift toward paid social signups [N1] rather than a site-wide onboarding failure, since load time and support tickets are normal.",
   "persistence": "unclear",
   "check": "Activation by acquisition channel for W37 vs W38, plus activation for the N2 test arm vs control within organic signups only."
  },
  "... 1 more"
 ],
 "actions": [
  {
   "type": "investigate",
   "action": "Break W38 activation down by acquisition channel to confirm whether the drop is paid social mix or a rate fall within existing channels.",
   "ref": "M2",
   "success_metric": "Organic and referral activation within their prior-week range while blended activation falls, confirming mix; or a clear per-channel rate drop pointing to onboarding."
  },
  {
   "type": "investigate",
   "action": "Compare activation for workspaces with and without the new onboarding checklist copy [N2], within the same channel, to rule the copy change in or out.",
   "ref": "M2",
   "success_metric": "No significant activation gap between arms clears N2; a gap of a point or more in the test arm means roll it back."
  },
  "... 2 more"
 ],
 "caveats": [
  "W38 is not comparable to prior weeks: the paid social campaign tripled budget [N1] and an onboarding copy test ran on 50% of new workspaces [N2] in the same week, so channel mix and onboarding are confounded until cut separately.",
  "Partner webinars were paused in W37 [N3], which may be shrinking a historically engaged channel and colouring the W37-W38 comparison.",
  "... 2 more"
 ],
 "drill_next": {
  "ref": "M2",
  "why": "It is the only red metric, the biggest anomaly at -10.63 z, and the question hinges on whether the drop is mix from the paid social surge or a real per-user onboarding problem.",
  "breakdown": "Activation by acquisition channel (organic, referral, paid social, partner), with signups per channel as the base, W37 vs W38."
 },
 "prescan_responses": [
  {
   "id": "P1",
   "status": "confirmed",
   "reason": "Activation at 37.1% is 4.9 points below its 42% target and is the headline issue of this review."
  },
  {
   "id": "P2",
   "status": "confirmed",
   "reason": "The -10.63 z break is real relative to a very tight 41.2-42.6% history; the question is cause, which likely traces to the channel mix shift from N1."
  },
  "... 4 more"
 ]
}
{
 "lane": "drill",
 "metric_ref": "M2",
 "verdict": "mix",
 "headline": "Activation fell because the tripled paid social campaign [N1] flooded W38 with a channel that activates at about 21%, not because onboarding broke: channels' own rates barely moved.",
 "decomposition": "Blended activation fell 5.3791 points, from 42.5214% to 37.1423%, while signups grew 33.33% from 2340 to 3120. Of that fall, -4.8851 points is mix (the weight of lower-activating segments rising), only -0.3085 points is rate (segments' own activation changing at the old weights), and -0.1854 points is interaction. Paid social alone contributes -4.8621 points as its share of signups jumped from 10.26% to 32.37% while its own rate slipped just 1 point from 22% to 21%.",
 "drivers": [
  {
   "ref": "S3",
   "contribution": -4.8621,
   "explanation": "Paid social signups rose from 240 to 1010 and their share from 10.26% to 32.37%, at an activation rate of 21%, roughly half the blended average; almost all of its impact is mix (-4.5384) rather than rate (-0.1026). This is consistent with the W38 'Dashboards in 5 minutes' campaign tripling budget [N1].",
   "confidence": "high"
  },
  {
   "ref": "S1",
   "contribution": -0.4037,
   "explanation": "Organic activation eased 0.3 points to 44.2% and its share fell from 59.83% to 46.47% as paid social grew; most of the contribution is mix (-0.2642), with a small rate slip (-0.1795) that could reflect the onboarding copy test [N2] or noise.",
   "confidence": "medium"
  },
  "... 1 more"
 ],
 "offsets": [
  {
   "ref": "S4",
   "text": "Partner activation rose 0.4 points to 38.4% and its shrinking share of a below-average segment (320 to 260 signups, consistent with the webinar pause [N3]) adds 0.2749 points back to the blend."
  }
 ],
 "hypotheses": [
  {
   "hypothesis": "The drop is a cohort mix effect from the paid social campaign bringing lower-intent signups, not an onboarding failure.",
   "evidence": "S3,N1,P7,P9",
   "test": "Recompute W38 activation at W37 channel weights; if it lands near 42.2% (prior minus the -0.3085 rate effect), onboarding is intact and the mix explanation holds."
  },
  {
   "hypothesis": "The onboarding checklist copy change [N2] is causing a small real rate decline across channels.",
   "evidence": "S1,S2,N2",
   "test": "Compare activation for test vs control workspaces within organic and referral signups in W38; a consistent gap of more than the observed 0.3-0.5 points would implicate the copy."
  },
  "... 1 more"
 ],
 "validation": [
  "Segments reconcile to the top line (reconciles is true, no folded segments), though the drill's W37 total of 42.5214% differs slightly from the scorecard's 42.4%, likely rounding or a slightly different base.",
  "The base grew 33.33% (2340 to 3120), almost all from paid social (240 to 1010), so this is a different population week on week and blended comparisons to history are not like for like.",
  "... 2 more"
 ],
 "next_queries": [
  "W38 activation by channel crossed with onboarding test arm (N2 test vs control), with signups per cell as the base.",
  "Paid social W38 signups by ad set and landing page, with activation and week-1 retention per ad set.",
  "... 2 more"
 ],
 "recommendation": "Do not roll back onboarding on this evidence; first split W38 activation by the N2 test arm within organic signups to clear the copy change, and re-evaluate paid social targeting on cost per activated workspace rather than cost per signup.",
 "summary": "The 5.4-point activation drop is almost entirely mix: paid social went from 10.26% to 32.37% of signups at a 21% activation rate, contributing -4.8621 points, while every channel's own rate moved by a point or less. Onboarding does not look broken, but the campaign is buying signups that activate at half the normal rate, so the question for the team is paid social targeting and its first-run experience.",
 "prescan_responses": [
  {
   "id": "P1",
   "status": "confirmed",
   "reason": "The target miss is real at the blended level, but the drill shows it is driven by channel mix, not by per-channel activation falling."
  },
  {
   "id": "P2",
   "status": "confirmed",
   "reason": "The -10.63 z anomaly is explained by the paid social mix shift; it is a composition change rather than a behavioural break."
  },
  "... 3 more"
 ]
}

Truncation and partial results

When the balance sits between min_credits and hold_credits, the run is not refused: it executes with a reduced output cap and returns truncated: true. What you hold is a prefix of the reply. The web page closes the cut-off JSON (Recon.closeJson), shows the sections that arrived and says how many it recovered - out of eleven for a review and twelve for a drill-down. From code, check the flag before treating a reply as complete, then resubmit with the attempt suffix on the Idempotency-Key incremented.