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.
| task | what it does |
|---|---|
review | The 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. |
drill | The 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
| code | status | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Get a new one from the token page. |
payment_required | 402 | The balance is below min_credits. Call /estimate first and top up. |
forbidden | 403 | The token is valid but not for this app, or a guest token tried a metered run. |
not_found | 404 | Unknown job id, unknown collection, or the app slug does not exist. |
conflict | 409 | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
validation_error | 422 | A field is the wrong type. Every field is a string: pack and review must be JSON-encoded strings, not objects. |
rate_limited | 429 | Too many requests. Back off and retry; do not tight-loop. |
internal | 5xx | A 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"}}
# Open https://metrics-desk.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot start a metered run.
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest", data=b'{"slug": "metrics-desk"}', method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
// Open https://metrics-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "metrics-desk" }),
});
const TOKEN = (await res.json()).data.token;
// Open https://metrics-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
guestReq, _ := http.NewRequest(http.MethodPost,
"https://api.skillsafe.ai/v1/app-api/guest", bytes.NewReader([]byte(`{"slug":"metrics-desk"}`)))
guestReq.Header.Set("Content-Type", "application/json")
guestRes, err := http.DefaultClient.Do(guestReq)
if err != nil {
panic(err)
}
defer guestRes.Body.Close()
var guest struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
_ = json.NewDecoder(guestRes.Body).Decode(&guest)
fmt.Println(guest.Data.Token)
// Open https://metrics-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
var http = HttpClient.newHttpClient();
var guestReq = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"metrics-desk\"}"))
.build();
HttpResponse<String> guest = http.send(guestReq, HttpResponse.BodyHandlers.ofString());
System.out.println(guest.body()); // {"ok":true,"data":{"token":"…","subject_type":"guest"}}
# Open https://metrics-desk.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot start a metered run.
require "json"
require "net/http"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.generate({ slug: "metrics-desk" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
TOKEN = JSON.parse(res.body)["data"]["token"]
<?php
// Open https://metrics-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["slug" => "metrics-desk"]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$guest = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $guest["data"]["token"];
// Open https://metrics-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
using var http = new HttpClient();
var guestReq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/guest");
guestReq.Content = new StringContent("{\"slug\":\"metrics-desk\"}", Encoding.UTF8, "application/json");
var guestRes = await http.SendAsync(guestReq);
var guest = await guestRes.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(guest.GetProperty("data").GetProperty("token").GetString());
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
}
import json, os, urllib.error, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "metrics-desk"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://metrics-desk.skillsafe.ai/tokens.html
def call(path, body=None):
"""Returns the unwrapped `data`, or raises with the API error code."""
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(f"{BASE}/{path}", data=data, method="POST" if body is not None else "GET")
req.add_header("Authorization", f"Bearer {TOKEN}")
if body is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as e:
payload = json.load(e)
if not payload.get("ok"):
err = payload.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "metrics-desk";
const TOKEN = "YOUR_TOKEN"; // from https://metrics-desk.skillsafe.ai/tokens.html
async function call(path, body) {
const res = await fetch(`${BASE}/${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const payload = await res.json();
if (!payload.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
return payload.data;
}
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "metrics-desk"
)
var token = os.Getenv("SKILLSAFE_TOKEN") // from https://metrics-desk.skillsafe.ai/tokens.html
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if body != nil {
method = http.MethodPost
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+"/"+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class MetricsDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "metrics-desk";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN);
if (jsonBody != null) {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
b.GET();
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is always {"ok":true,"data":...} or {"ok":false,"error":...}.
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "metrics-desk"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://metrics-desk.skillsafe.ai/tokens.html
def call(path, body = nil)
uri = URI("#{BASE}/#{path}")
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "metrics-desk";
define("TOKEN", getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"); // from /tokens.html
function call(string $path, ?array $body = null) {
$ch = curl_init(BASE . "/" . $path);
$headers = ["Authorization: Bearer " . TOKEN];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
using System.Net.Http.Json;
using System.Text.Json;
static class MetricsDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "metrics-desk";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(string path, object? body = null)
{
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, $"{Base}/{path}");
req.Headers.Add("Authorization", $"Bearer {Token}");
if (body is not null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var payload = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!payload.GetProperty("ok").GetBoolean())
{
var e = payload.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return payload.GetProperty("data");
}
}
3. Check the session and the balance
call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
print(me["subject_type"], me.get("credits"))
const me = await call("me");
console.log(me.subject_type, me.credits);
raw, err := call("me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("me", null));
// {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var me = await MetricsDesk.Call("me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
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.
INPUT = json.load(open("body.json")) # built by make-body.js below
assert isinstance(INPUT, dict) and INPUT.get("task") in ("review", "drill")
assert all(isinstance(INPUT.get(k), str) and INPUT[k].strip() for k in ("product", "cadence", "pack"))
assert isinstance(json.loads(INPUT["pack"]), dict) # pack is a JSON STRING
est = call("estimate", INPUT)
print(est["model"], est["model_alias"], est["hold_credits"], est.get("warnings"))
me = call("me")
if me.get("credits", 0) < est["min_credits"]:
raise SystemExit("top up first: balance is below min_credits")
import { readFileSync } from "node:fs";
const INPUT = JSON.parse(readFileSync("body.json", "utf8")); // built by make-body.js below
if (!INPUT || typeof INPUT !== "object" || !["review", "drill"].includes(INPUT.task)) throw new Error("bad task");
for (const k of ["product", "cadence", "pack"]) if (typeof INPUT[k] !== "string") throw new Error(k + " must be a string");
const est = await call("estimate", INPUT);
console.log(est.model, est.model_alias, est.hold_credits, est.warnings);
const me = await call("me");
if ((me.credits ?? 0) < est.min_credits) throw new Error("top up first");
raw, _ := os.ReadFile("body.json") // built by make-body.js below
var input map[string]string // every field is a string, pack included
if err := json.Unmarshal(raw, &input); err != nil {
panic("body.json must be an object of strings: " + err.Error())
}
lane := input["task"] // "review" or "drill"
if lane != "review" && lane != "drill" {
panic("task must be review or drill")
}
est, err := call("estimate", input)
if err != nil {
panic(err)
}
fmt.Println(string(est)) // model, model_alias, markup_bps, hold_credits, min_credits
String input = Files.readString(Path.of("body.json")); // built by make-body.js below
String lane = input.replaceAll("(?s).*\"task\"\\s*:\\s*\"(review|drill)\".*", "$1");
if (!lane.equals("review") && !lane.equals("drill")) throw new IllegalStateException("task must be review or drill");
String est = call("estimate", input);
System.out.println(est); // model, model_alias, markup_bps, hold_credits, min_credits
INPUT = JSON.parse(File.read("body.json")) # built by make-body.js below
raise "bad task" unless %w[review drill].include?(INPUT["task"])
%w[product cadence pack].each { |k| raise "#{k} must be a string" unless INPUT[k].is_a?(String) }
est = call("estimate", INPUT)
puts est["model"], est["model_alias"], est["hold_credits"]
<?php
$input = json_decode(file_get_contents("body.json"), true); // built by make-body.js below
if (!is_array($input) || !in_array($input["task"] ?? "", ["review", "drill"], true)) { throw new Exception("bad task"); }
foreach (["product", "cadence", "pack"] as $k) { if (!is_string($input[$k] ?? null)) { throw new Exception("$k must be a string"); } }
$est = call("estimate", $input);
echo $est["model"], " ", $est["model_alias"], " ", $est["hold_credits"], PHP_EOL;
var input = File.ReadAllText("body.json"); // built by make-body.js below
using var doc = JsonDocument.Parse(input);
var lane = doc.RootElement.GetProperty("task").GetString();
if (lane != "review" && lane != "drill") throw new Exception("task must be review or drill");
var est = await Call("estimate", input);
Console.WriteLine(est); // model, model_alias, markup_bps, hold_credits, min_credits
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:
| field | required | what it holds |
|---|---|---|
task | yes | review or drill. |
product | yes | What the product is, in a few words. |
cadence | yes | weekly, monthly or quarterly. |
pack | yes | JSON 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. |
notes | no | Context lines, [N1] ...: launches, campaigns, incidents. |
question | no | What you want to know. |
review | no (drill only) | JSON string of the review's overall call and reading of the drilled metric. |
retry_note | no | Only 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
import hashlib, time
lane = INPUT["task"] # "review" or "drill"
digest = hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:16]
key = f"metrics-desk:{lane}:{digest}:a1"
req = urllib.request.Request(f"{BASE}/run", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call(f"jobs/{job_id}")
if job["status"] in ("succeeded", "failed"):
break
time.sleep(2)
if job["status"] == "failed":
raise RuntimeError(job.get("error"))
reply = json.loads(job["output"]["output"])
print(reply["lane"], reply.get("overall") or reply.get("verdict"), reply["headline"])
print("charged", job.get("charged_credits"), "truncated", job.get("truncated"))
import { createHash } from "node:crypto";
const lane = INPUT.task; // "review" or "drill"
const digest = createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16);
const key = `metrics-desk:${lane}:${digest}:a1`;
const started = await fetch(`${BASE}/run`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify(INPUT),
}).then((r) => r.json());
let job = started.data;
while (job.status !== "succeeded" && job.status !== "failed") {
await new Promise((r) => setTimeout(r, 2000));
job = await call(`jobs/${job.job_id}`);
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const reply = JSON.parse(job.output.output);
console.log(reply.lane, reply.overall ?? reply.verdict, reply.headline, job.charged_credits);
body, _ := json.Marshal(input)
sum := sha256.Sum256(body)
key := fmt.Sprintf("metrics-desk:%s:%x:a1", lane, sum[:8])
req, _ := http.NewRequest(http.MethodPost, base+"/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
var started struct {
Data struct {
JobID string `json:"job_id"`
} `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&started)
res.Body.Close()
var jobOutput string
for {
raw, err := call("jobs/"+started.Data.JobID, nil)
if err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
Charged int `json:"charged_credits"`
}
_ = json.Unmarshal(raw, &job)
if job.Status == "succeeded" {
jobOutput = job.Output.Output
fmt.Println(job.Charged)
break
}
if job.Status == "failed" {
panic(string(raw))
}
time.Sleep(2 * time.Second)
}
// lane was read and checked in step 4: "review" or "drill".
String key = "metrics-desk:" + lane + ":" + sha256Hex(input).substring(0, 16) + ":a1";
HttpRequest run = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(input)).build();
String started = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
String jobId = started.replaceAll(".*\"job_id\":\"([^\"]+)\".*", "$1");
while (true) {
String job = call("jobs/" + jobId, null);
if (job.contains("\"status\":\"succeeded\"")) { System.out.println(job); break; }
if (job.contains("\"status\":\"failed\"")) throw new RuntimeException(job);
Thread.sleep(2000);
}
// Parse data.output.output (a string holding the reply JSON) with your JSON library.
// sha256Hex: HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(input.getBytes(UTF_8)))
require "digest"
lane = INPUT["task"]
key = "metrics-desk:#{lane}:#{Digest::SHA256.hexdigest(JSON.generate(INPUT))[0, 16]}:a1"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = JSON.generate(INPUT)
job = JSON.parse(Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }.body)["data"]
until %w[succeeded failed].include?(job["status"])
sleep 2
job = call("jobs/#{job['job_id']}")
end
raise job.inspect if job["status"] == "failed"
reply = JSON.parse(job["output"]["output"])
puts reply["lane"], reply["headline"]
<?php
$key = "metrics-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json", "Idempotency-Key: " . $key],
CURLOPT_RETURNTRANSFER => true,
]);
$job = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
while (!in_array($job["status"], ["succeeded", "failed"], true)) {
sleep(2);
$job = call("jobs/" . $job["job_id"]);
}
$reply = json_decode($job["output"]["output"], true);
echo $reply["lane"], " ", $reply["headline"], PHP_EOL;
using System.Security.Cryptography;
var json = JsonSerializer.Serialize(input);
var lane = input.GetProperty("task").GetString();
var key = $"metrics-desk:{lane}:" + Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(json)))[..16].ToLower() + ":a1";
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run");
req.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN"}");
req.Headers.Add("Idempotency-Key", key);
req.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var started = await (await new HttpClient().SendAsync(req)).Content.ReadFromJsonAsync<JsonElement>();
var jobId = started.GetProperty("data").GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await MetricsDesk.Call($"jobs/{jobId}");
var status = job.GetProperty("status").GetString();
if (status == "succeeded") break;
if (status == "failed") throw new Exception(job.ToString());
await Task.Delay(2000);
}
var reply = JsonSerializer.Deserialize<JsonElement>(job.GetProperty("output").GetProperty("output").GetString()!);
Console.WriteLine($"{reply.GetProperty("lane")} {reply.GetProperty("headline")}");
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}
req = urllib.request.Request(f"{BASE}/run-stream", data=json.dumps(INPUT).encode(), method="POST")
for h, v in (("Authorization", f"Bearer {TOKEN}"), ("Content-Type", "application/json"),
("Idempotency-Key", key), ("Accept", "text/event-stream")):
req.add_header(h, v)
raw, done, event = "", {}, None
with urllib.request.urlopen(req) as stream:
for line in stream:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
raw += json.loads(line[6:]).get("text", "")
elif line.startswith("data: ") and event == "done":
done = json.loads(line[6:])
print(done.get("status"), done.get("charged_credits"), done.get("truncated"))
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key, Accept: "text/event-stream" },
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null, done = null;
for (;;) {
const { value, done: end } = await reader.read();
if (end) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i); buf = buf.slice(i + 1);
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ") && event === "delta") raw += JSON.parse(line.slice(6)).text || "";
else if (line.startsWith("data: ") && event === "done") done = JSON.parse(line.slice(6));
}
}
console.log(done, raw.length);
req, _ = http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, err = http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var raw strings.Builder
event := ""
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = line[7:]
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct{ Text string `json:"text"` }
_ = json.Unmarshal([]byte(line[6:]), &d)
raw.WriteString(d.Text)
case strings.HasPrefix(line, "data: ") && event == "done":
fmt.Println("done:", line[6:])
}
}
HttpRequest stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(input)).build();
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
// "event: delta" lines are followed by "data: {\"text\":...}"; "event: done" by the status.
if (line.startsWith("data: ")) System.out.println(line.substring(6));
});
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
{ "Authorization" => "Bearer #{TOKEN}", "Content-Type" => "application/json",
"Idempotency-Key" => key, "Accept" => "text/event-stream" }.each { |k, v| req[k] = v }
req.body = JSON.generate(INPUT)
raw, event = +"", nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |h|
h.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event: ") then event = line[7..]
elsif line.start_with?("data: ") && event == "delta" then raw << JSON.parse(line[6..])["text"].to_s
elsif line.start_with?("data: ") && event == "done" then puts line[6..]
end
end
end
end
end
<?php
$raw = ""; $event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json", "Idempotency-Key: " . $key, "Accept: text/event-stream"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) $event = substr($line, 7);
elseif (str_starts_with($line, "data: ") && $event === "delta") $raw .= json_decode(substr($line, 6), true)["text"] ?? "";
elseif (str_starts_with($line, "data: ") && $event === "done") echo substr($line, 6), PHP_EOL;
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
var sreq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream");
sreq.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN"}");
sreq.Headers.Add("Idempotency-Key", key);
sreq.Headers.Add("Accept", "text/event-stream");
sreq.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
using var sres = await new HttpClient().SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var sr = new StreamReader(await sres.Content.ReadAsStreamAsync());
var raw = new System.Text.StringBuilder(); string? ev = null, line;
while ((line = await sr.ReadLineAsync()) != null)
{
if (line.StartsWith("event: ")) ev = line[7..];
else if (line.StartsWith("data: ") && ev == "delta") raw.Append(JsonSerializer.Deserialize<JsonElement>(line[6..]).GetProperty("text").GetString());
else if (line.StartsWith("data: ") && ev == "done") Console.WriteLine(line[6..]);
}
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
text = job["output"]["output"]
reply = json.loads(text[text.index("{"):text.rindex("}") + 1])
if reply.get("lane") == "drill":
print(reply["verdict"], [(d["ref"], d["contribution"]) for d in reply["drivers"]])
else:
print(reply["overall"], [(s["ref"], s["status"]) for s in reply["scorecard"]])
print([a["action"] for a in reply["actions"]])
const text = job.output.output;
const reply = JSON.parse(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1));
if (reply.lane === "drill") console.log(reply.verdict, reply.drivers.map((d) => [d.ref, d.contribution]));
else console.log(reply.overall, reply.scorecard.map((s) => [s.ref, s.status]), reply.actions.map((a) => a.action));
start, end := strings.Index(jobOutput, "{"), strings.LastIndex(jobOutput, "}")
var reply map[string]any
if err := json.Unmarshal([]byte(jobOutput[start:end+1]), &reply); err != nil {
panic(err)
}
fmt.Println(reply["lane"], reply["headline"])
if reply["lane"] == "drill" {
fmt.Println(reply["verdict"], reply["drivers"])
} else {
fmt.Println(reply["overall"], reply["scorecard"], reply["actions"])
}
// output is data.output.output from step 5: a string holding the reply JSON.
String json = output.substring(output.indexOf('{'), output.lastIndexOf('}') + 1);
// With Jackson: JsonNode r = new ObjectMapper().readTree(json);
// review: r.get("overall"), r.get("scorecard"), r.get("actions")
// drill: r.get("verdict"), r.get("drivers") - each driver's "contribution" is a number
System.out.println(json);
text = job["output"]["output"]
reply = JSON.parse(text[text.index("{")..text.rindex("}")])
if reply["lane"] == "drill"
puts reply["verdict"], reply["drivers"].map { |d| "#{d['ref']} #{d['contribution']}" }
else
puts reply["overall"], reply["scorecard"].map { |s| "#{s['ref']} #{s['status']}" }
end
<?php
$text = $job["output"]["output"];
$reply = json_decode(substr($text, strpos($text, "{"), strrpos($text, "}") - strpos($text, "{") + 1), true);
if (($reply["lane"] ?? "") === "drill") {
foreach ($reply["drivers"] as $d) { echo $d["ref"], " ", $d["contribution"], PHP_EOL; }
} else {
foreach ($reply["scorecard"] as $s) { echo $s["ref"], " ", $s["status"], PHP_EOL; }
}
var text = output; // data.output.output from step 5
var json = text.Substring(text.IndexOf('{'), text.LastIndexOf('}') - text.IndexOf('{') + 1);
using var reply = JsonDocument.Parse(json);
var r = reply.RootElement;
Console.WriteLine($"{r.GetProperty("lane")} {r.GetProperty("headline")}");
if (r.GetProperty("lane").GetString() == "drill")
foreach (var d in r.GetProperty("drivers").EnumerateArray()) Console.WriteLine($"{d.GetProperty("ref")} {d.GetProperty("contribution")}");
else
foreach (var s in r.GetProperty("scorecard").EnumerateArray()) Console.WriteLine($"{s.GetProperty("ref")} {s.GetProperty("status")}");
Invariants worth asserting
- Review: exactly one
scorecardentry per metric; ananomaliesentry for every metric with |z_score| >= 2;drill_next.refis a metric id;on_tracknever with a red metric. - Drill:
metric_refequalspack.drill.metric; every driver'scontributionequals that segment'scontribution; the first driver istop_segment;verdictequalsverdict_hintunless it isinconclusive. - Both: every ref (
M,S,N,P) exists in the input, and every prescan flag has exactly one response.
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.