Driving Pulse Desk from your own code
Everything the web app does is available over HTTP. Paste a time series, pick a lane, and get back one JSON object. The deterministic statistics the browser runs for free - the OLS trend and its significance test, autocorrelation seasonality, the exact single-changepoint search, z-score anomalies, the volatility-regime read, the naive forecast - are not recomputed server-side, so if you drive the API directly you should send your own prescan facts: that is what the model is held accountable to.
Base URL and headers
https://api.skillsafe.ai/v1/app-api
One header on every request:
Authorization: Bearer <token>— get one from the token page, no developer console needed.
The token is app-scoped, so the slug is not a header. There is no X-App-Slug header — a token minted for this app addresses this app and nothing else. The slug appears in exactly one place: the body of POST /guest, which is how you get a token in the first place.
curl -sS -X POST https://api.skillsafe.ai/v1/app-api/guest \
-H "Content-Type: application/json" \
-d '{"slug": "pulse-desk"}'
# {"ok":true,"data":{"token":"aut_...","guest_id":"gst_...","expires_at":"..."}}
A guest token is enough for /me and /estimate. Running either lane is metered and needs a personal token, which comes from signing in on the token page.
The body of /estimate, /run and /run-stream is the input object itself, not wrapped in an input key. Its fields are listed under step 3 below.
The response envelope
Every response has the same two shapes. Branch on error.code, never on the message text — messages are for humans and will change.
// success
{"ok": true, "data": { ... }}
// failure
{"ok": false, "error": {"code": "VALIDATION_ERROR",
"message": "human-readable",
"details": { ... }}}
Error codes
| code | HTTP | What it means and what to do |
|---|---|---|
UNAUTHORIZED | 401 | No token, a malformed token, or a token for a different app. Mint a new one from the token page. |
FORBIDDEN | 403 | A guest token on a metered lane. Sign in for a personal token, or ask the publisher to enable sponsorship. |
NOT_FOUND | 404 | The job id does not exist, or the token belongs to a different app. |
VALIDATION_ERROR | 400 | The input failed validation. error.details names the offending field - usually task set to something outside diagnostics/brief. |
PAYMENT_REQUIRED | 402 | The balance is below min_credits. Never let a user reach this: compare hold_credits against /me first. |
RATE_LIMITED | 429 | Too many requests. Back off and retry with a growing delay; the app-api budget is shared across your whole account. |
INTERNAL | 500 | A platform fault. Retry once with the same Idempotency-Key so you are not billed twice. |
1. A tiny client helper, and where the token comes from
Get the token first: open the token page, sign in, and copy it — that page reads and writes the token this browser already holds for pulse-desk, so no developer console is involved. Then two headers on every call: the bearer token and, where the call takes a body, the content type. Success is always {"ok": true, "data": {...}}; a failure carries error.code, so branch on the code and not on the message text.
# Every call needs two things: the app slug and a bearer token.
# Keep the token in a shell variable so it never lands in your history.
SLUG="pulse-desk"
TOKEN="YOUR_TOKEN" # from https://pulse-desk.skillsafe.ai/tokens.html
BASE="https://api.skillsafe.ai/v1/app-api"
# A tiny helper: $1 is the path, $2 is the JSON body (optional).
ssapp() {
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
import urllib.request
SLUG = "pulse-desk"
TOKEN = "YOUR_TOKEN" # from https://pulse-desk.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
class AppError(Exception):
"""Carries the platform's error code so callers can branch on it."""
def __init__(self, code, message, details=None):
super().__init__(f"{code}: {message}")
self.code, self.message, self.details = code, message, details
def call(path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(f"{BASE}/{path}", data=data, method="POST" if data else "GET")
req.add_header("Authorization", f"Bearer {TOKEN}")
if data:
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)
err = payload.get("error") or {}
raise AppError(err.get("code", "unknown"), err.get("message", str(e)), err.get("details"))
# Success is always {"ok": true, "data": {...}}.
return payload["data"]
const SLUG = "pulse-desk";
const TOKEN = "YOUR_TOKEN"; // from https://pulse-desk.skillsafe.ai/tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
class AppError extends Error {
constructor(code, message, details) {
super(`${code}: ${message}`);
this.code = code;
this.details = details;
}
}
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 (!res.ok) {
const e = payload.error || {};
throw new AppError(e.code || "unknown", e.message || res.statusText, e.details);
}
return payload.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const (
slug = "pulse-desk"
token = "YOUR_TOKEN" // from https://pulse-desk.skillsafe.ai/tokens.html
base = "https://api.skillsafe.ai/v1/app-api"
)
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
Details json.RawMessage `json:"details"`
} `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, err := json.Marshal(body)
if err != nil {
return nil, err
}
rdr = bytes.NewReader(b)
}
req, err := http.NewRequest(method, base+"/"+path, rdr)
if err != nil {
return nil, err
}
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.Error != nil {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
public final class PulseDesk {
static final String SLUG = "pulse-desk";
static final String TOKEN = "YOUR_TOKEN"; // from https://pulse-desk.skillsafe.ai/tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
/** Returns the raw JSON body. Use your JSON library of choice to read it. */
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.GET();
} else {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
}
HttpResponse res = CLIENT.send(b.build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) {
throw new IllegalStateException("HTTP " + res.statusCode() + ": " + res.body());
}
return res.body();
}
}
require "json"
require "net/http"
require "uri"
SLUG = "pulse-desk"
TOKEN = "YOUR_TOKEN" # from https://pulse-desk.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
class AppError < StandardError
attr_reader :code, :details
def initialize(code, message, details = nil)
super("#{code}: #{message}")
@code = code
@details = details
end
end
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)
unless res.is_a?(Net::HTTPSuccess)
e = payload["error"] || {}
raise AppError.new(e["code"] || "unknown", e["message"] || res.message, e["details"])
end
payload["data"]
end
<?php
const SLUG = "pulse-desk";
const TOKEN = "YOUR_TOKEN"; // from https://pulse-desk.skillsafe.ai/tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
class AppError extends Exception {
public string $errorCode;
public $details;
public function __construct(string $code, string $message, $details = null) {
parent::__construct("$code: $message");
$this->errorCode = $code;
$this->details = $details;
}
}
function call(string $path, ?array $body = null) {
$headers = ["Authorization: Bearer " . TOKEN, ];
$opts = ["http" => ["method" => $body === null ? "GET" : "POST",
"ignore_errors" => true]];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
$opts["http"]["content"] = json_encode($body);
}
$opts["http"]["header"] = implode("\r\n", $headers);
$raw = file_get_contents(BASE . "/" . $path, false, stream_context_create($opts));
$payload = json_decode($raw, true);
if (isset($payload["error"])) {
$e = $payload["error"];
throw new AppError($e["code"] ?? "unknown", $e["message"] ?? "request failed",
$e["details"] ?? null);
}
return $payload["data"];
}
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public static class PulseDesk
{
const string Slug = "pulse-desk";
const string Token = "YOUR_TOKEN"; // from https://pulse-desk.skillsafe.ai/tokens.html
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Client = new HttpClient();
public static async Task Call(string path, object body = null)
{
var req = new HttpRequestMessage(body == null ? HttpMethod.Get : HttpMethod.Post,
$"{Base}/{path}");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (body != null)
{
req.Content = new StringContent(JsonSerializer.Serialize(body),
Encoding.UTF8, "application/json");
}
var res = await Client.SendAsync(req);
var text = await res.Content.ReadAsStringAsync();
var payload = JsonDocument.Parse(text).RootElement;
if (payload.TryGetProperty("error", out var err))
{
throw new InvalidOperationException(
$"{err.GetProperty("code").GetString()}: {err.GetProperty("message").GetString()}");
}
return payload.GetProperty("data");
}
}
2. Who am I, and can I afford it
GET /me is free. subject_type is user for a personal token and guest for an anonymous one. Only a personal token can run either lane, and credits is the balance you compare the hold against.
ssapp me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":184250,
# "app":{"slug":"pulse-desk","model":"gpt-terra","markup_bps":1000}}}
me = call("me")
print(me["subject_type"], me["credits"], "credits")
const me = await call("me");
console.log(me.subject_type, me.credits, "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, "credits")
String me = PulseDesk.call("me", null);
System.out.println(me);
me = call("me")
puts "#{me["subject_type"]} #{me["credits"]} credits"
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], " credits\n";
var me = await PulseDesk.Call("me");
Console.WriteLine($"{me.GetProperty("subject_type").GetString()} " +
$"{me.GetProperty("credits").GetInt32()} credits");
3. The input fields
The same object goes to /estimate, /run and /run-stream. task selects the lane and comes first.
| field | type | required | what it is |
|---|---|---|---|
task | string | yes | "diagnostics" or "brief". The full lane list is exactly those two: diagnostics reads the shape the free engine measured and says what it means; brief turns that reading into a decision brief for a stated purpose. Anything else is a VALIDATION_ERROR. |
series_text | string | yes | The pasted series, one point per line, as timestamp,value (comma, tab or whitespace separated) or a bare value with no timestamp at all. A header row such as date,value is auto-detected and skipped. A missing value is NA, null, or simply blank. |
value_label | string | no | Free text naming what the metric is, e.g. "daily p95 API latency, in milliseconds". It governs wording and unit-awareness, never arithmetic. |
purpose | string | no | One of ops-monitoring, business-kpi, quality-control, capacity-planning, other. Read by the brief lane, which echoes it back in body.purpose. |
prescan | object | yes | The exact deterministic statistics the app's free browser engine computed. Driving the API directly means computing and sending this yourself; it is what the model is held to. See the field list just below. |
prior_diagnostics | object | no | brief lane only. The diagnostics lane's own prior output on the same series - verdict, key_signals, and a trimmed findings list - so the two lanes read as one sitting. This is the handoff the web app's button performs. |
The prescan object
One object, ten keys, all computed before any model call. The two lanes above are the teaching surface of this page; prescan is plumbing you copy from the engine rather than hand-write, so here it is at the level of its keys. Run the free engine once on skillsafe.ai/pulse-desk and read the network tab, or read pulselib.js in the bundle, for every leaf field. The worked example in step 7 shows one real, complete prescan.
counts—points,lines_total,lines_clipped,invalid_lines,missing_values,duplicate_timestamps.series_meta— whether the series had labels, whether they parse as dates, whether they are monotonic and regularly spaced, and whether a header row was skipped.stats—n,mean,median,std,min,max,first,last, and the first-to-last change in absolute and percentage terms.trend— the OLS fit:slope,intercept,r_squared,t_stat,significant,direction, and the fitted change across the series.seasonality— autocorrelation on the detrended residuals: which lags were checked, the best period and its ACF, whether a cycle wasdetected, and the plain-languagereasonwhen none was.volatility—first_half_std,second_half_std, theirratio, and aregimeword.changepoint— the single best split from an exact SSE-reduction search:index,timestamp_label,mean_before,mean_after, the delta,explained_ratio, and the point count on each side.anomalies— acountand atoparray of the strongest z-score outliers.forecast— the naive one-step-ahead point with an indicative interval and themethodthat produced it.flags— the engine's own raised flags, each{id, severity, label}. Every flag id you send comes back in the reply'sreconciliationarray, so the model cannot quietly ignore one.
4. Price it before you run it
POST /estimate is free and creates no job. It returns the model binding and hold_credits - the amount reserved, which is almost always more than the settled charge because the hold prices the full output cap. The hold differs per lane, so re-estimate whenever you change task. Both lanes are shown below.
# The prescan fields shown here are trimmed for readability - send the real ones your engine computed.
read -r -d '' INPUT <<'JSON'
{
"task": "diagnostics",
"series_text": "2026-01-01,118\n2026-01-02,121\n2026-01-03,119\n2026-01-20,120\n2026-01-21,338\n2026-01-30,342",
"value_label": "daily p95 API latency, in milliseconds",
"purpose": "ops-monitoring",
"prescan": {"ok": true, "counts": {"points": 30, "missing_values": 0, "invalid_lines": 0},
"stats": {"n": 30, "mean": 193.2, "median": 121.5, "std": 105.4924, "min": 116, "max": 349},
"trend": {"slope": 9.8073, "r_squared": 0.6698, "significant": true, "direction": "up"},
"seasonality": {"detected": false, "best_period": null, "best_acf": null},
"volatility": {"first_half_std": 2.3664, "second_half_std": 107.2466, "ratio": 45.32, "regime": "increased"},
"changepoint": {"detected": true, "index": 20, "timestamp_label": "2026-01-21",
"mean_before": 119.9, "mean_after": 339.8, "explained_ratio": 0.9989},
"anomalies": {"count": 0, "top": []},
"forecast": {"method": "linear-trend", "next_label": "2026-01-31", "next_point": 345.2138},
"flags": [{"id": "CHANGEPOINT_FOUND", "severity": "medium", "label": "..."}]}
}
JSON
ssapp estimate "$INPUT"
# {"ok":true,"data":{"model":"gpt-terra","model_alias":"gpt-terra","markup_bps":1000,
# "hold_credits":2140,"min_credits":310,"sponsor_enabled":false}}
# The other lane: same series, task swapped, plus the handoff object. Re-estimate - the hold differs.
BRIEF=$(printf '%s' "$INPUT" | sed 's/"diagnostics"/"brief"/')
ssapp estimate "$BRIEF"
series = {
"series_text": "2026-01-01,118\n2026-01-02,121\n2026-01-21,338\n2026-01-30,342",
"value_label": "daily p95 API latency, in milliseconds",
"purpose": "ops-monitoring",
# Trimmed for readability - send the real object your engine computed.
"prescan": {"ok": True, "counts": {"points": 30}, "flags": []},
}
for task in ("diagnostics", "brief"):
est = call("estimate", {"task": task, **series})
print(task, est["hold_credits"], "credits reserved")
const series = {
series_text: "2026-01-01,118\n2026-01-02,121\n2026-01-21,338\n2026-01-30,342",
value_label: "daily p95 API latency, in milliseconds",
purpose: "ops-monitoring",
// Trimmed for readability - send the real object your engine computed.
prescan: { ok: true, counts: { points: 30 }, flags: [] }
};
for (const task of ["diagnostics", "brief"]) {
const est = await call("estimate", { task, ...series });
console.log(task, est.hold_credits, "credits reserved");
}
for _, task := range []string{"diagnostics", "brief"} {
raw, err := call("estimate", map[string]any{
"task": task,
"series_text": "2026-01-01,118\n2026-01-02,121\n2026-01-21,338\n2026-01-30,342",
"value_label": "daily p95 API latency, in milliseconds",
"purpose": "ops-monitoring",
// Trimmed for readability - send the real object your engine computed.
"prescan": map[string]any{"ok": true, "flags": []any{}},
})
if err != nil {
panic(err)
}
fmt.Println(task, string(raw))
}
String series = "\"series_text\":\"2026-01-01,118\\n2026-01-21,338\\n2026-01-30,342\","
+ "\"value_label\":\"daily p95 API latency, in milliseconds\","
+ "\"purpose\":\"ops-monitoring\","
// Trimmed for readability - send the real object your engine computed.
+ "\"prescan\":{\"ok\":true,\"flags\":[]}";
for (String task : new String[] {"diagnostics", "brief"}) {
String est = PulseDesk.call("estimate", "{\"task\":\"" + task + "\"," + series + "}");
System.out.println(task + " " + est);
}
series = {
series_text: "2026-01-01,118\n2026-01-02,121\n2026-01-21,338\n2026-01-30,342",
value_label: "daily p95 API latency, in milliseconds",
purpose: "ops-monitoring",
# Trimmed for readability - send the real object your engine computed.
prescan: { ok: true, counts: { points: 30 }, flags: [] }
}
%w[diagnostics brief].each do |task|
est = call("estimate", series.merge(task: task))
puts "#{task} #{est["hold_credits"]} credits reserved"
end
$series = [
"series_text" => "2026-01-01,118\n2026-01-02,121\n2026-01-21,338\n2026-01-30,342",
"value_label" => "daily p95 API latency, in milliseconds",
"purpose" => "ops-monitoring",
// Trimmed for readability - send the real object your engine computed.
"prescan" => ["ok" => true, "counts" => ["points" => 30], "flags" => []]
];
foreach (["diagnostics", "brief"] as $task) {
$est = call("estimate", array_merge(["task" => $task], $series));
echo $task, " ", $est["hold_credits"], " credits reserved\n";
}
foreach (var task in new[] { "diagnostics", "brief" })
{
var est = await PulseDesk.Call("estimate", new {
task,
series_text = "2026-01-01,118\n2026-01-02,121\n2026-01-21,338\n2026-01-30,342",
value_label = "daily p95 API latency, in milliseconds",
purpose = "ops-monitoring",
// Trimmed for readability - send the real object your engine computed.
prescan = new { ok = true, flags = Array.Empty
5. Run it and poll, or stream it
POST /run returns a job_id immediately; poll GET /jobs/{id} until status is terminal. POST /run-stream is Server-Sent Events - deltas arrive as they are generated, which is what the web app uses for the staged progress card. Both need an Idempotency-Key header derived from (task, series_text, value_label, purpose, attempt): reuse the same key on a retry of the same logical run so a network blip or a malformed first reply can never double-bill.
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: pulse-desk:diagnostics:$(echo -n "$INPUT" | shasum | cut -c1-16):a1" \
-d "$INPUT" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')
until curl -sS "$BASE/jobs/$JOB" -H "Authorization: Bearer $TOKEN" \
| tee /tmp/job.json | python3 -c 'import json,sys;d=json.load(sys.stdin)["data"];exit(0 if d["status"] in ("succeeded","failed") else 1)'
do sleep 1; done
cat /tmp/job.json
# Streaming instead:
curl -sS -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: pulse-desk:diagnostics:$(echo -n "$INPUT" | shasum | cut -c1-16):a1" \
-d "$INPUT"
# text/event-stream: a sequence of "data: {...}" lines, terminated by a final job event.
import hashlib, time
def idem_key(task, input_obj, attempt=1):
h = hashlib.sha256(json.dumps([task, input_obj.get("series_text"), input_obj.get("value_label"),
input_obj.get("purpose")]).encode()).hexdigest()[:16]
return f"pulse-desk:{task}:{h}:a{attempt}"
def run_and_wait(input_obj):
req = urllib.request.Request(f"{BASE}/run", data=json.dumps(input_obj).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", idem_key(input_obj["task"], input_obj))
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"):
return job
time.sleep(1)
async function idemKey(task, input) {
const enc = new TextEncoder().encode(JSON.stringify([task, input.series_text, input.value_label, input.purpose]));
const digest = await crypto.subtle.digest("SHA-256", enc);
const hex = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
return `pulse-desk:${task}:${hex}:a1`;
}
async function runStream(input, onDelta) {
const key = await idemKey(input.task, input);
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify(input)
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
// Parse "data: {...}\n\n" frames from buf and call onDelta per delta event.
}
}
// Poll variant: POST /run, then GET /jobs/{id} until status is terminal.
raw, err := call("run", input) // set the Idempotency-Key header inside call() for this request
// ... unmarshal raw to get job_id, then loop GET("jobs/"+jobID) with a short sleep.
// Poll variant: POST run, read job_id, then GET jobs/{id} in a loop with Thread.sleep(1000)
// until status is "succeeded" or "failed". Add the Idempotency-Key header alongside Authorization.
def idem_key(task, input)
h = Digest::SHA256.hexdigest([task, input[:series_text], input[:value_label], input[:purpose]].to_json)[0, 16]
"pulse-desk:#{task}:#{h}:a1"
end
# POST to "run" with that header, read job_id, then GET "jobs/#{job_id}" until status is terminal.
// Poll variant: POST to "run" with an Idempotency-Key header derived the same way,
// read job_id from the response, then GET "jobs/{$jobId}" in a loop until status is terminal.
// Poll variant: POST to "run" with an Idempotency-Key header, read job_id,
// then GET "jobs/{jobId}" in a loop with Task.Delay(1000) until status is terminal.
6. Worked example: the diagnostics task
A real 30-point daily p95 API latency series, flat around 120ms for three weeks then stepped up to about 340ms. Request (trimmed - see step 3 for the full prescan shape):
{
"task": "diagnostics",
"series_text": "2026-01-01,118\n2026-01-02,121\n...\n2026-01-20,120\n2026-01-21,338\n...\n2026-01-30,342",
"value_label": "daily p95 API latency, in milliseconds",
"purpose": "ops-monitoring",
"prescan": { "...": "the full object from step 3, computed by pulselib.js" }
}
A real captured response (unedited, from an actual run over the series above):
{
"task": "diagnostics", "task_inferred": false,
"title": "Daily p95 API latency (ms): level-shifted from ~120ms to ~340ms at 2026-01-21, not a gradual climb",
"verdict": "shifting",
"summary": "For the first 20 days daily p95 API latency held a tight band around 116-124ms. On 2026-01-21 the mean jumped to roughly 340ms and stayed there, a change of about +220ms (+183%) that the changepoint search says explains almost all of the variance in the whole series (explained_ratio 0.9989). ...",
"assumptions": ["Assumed the pasted values are already the daily p95 aggregate described by the value_label, with no unstated change in units or measurement window at 2026-01-21."],
"open_questions": ["Was there a deploy, configuration change, or traffic-pattern shift around 2026-01-21 that would explain the level shift? (The pasted numbers do not say.)"],
"findings": [
{"id": "PD-001", "severity": "high", "point": "2026-01-21", "title": "Mean latency roughly tripled on 2026-01-21",
"why": "prescan.changepoint places the single best regime split at 2026-01-21, with mean_before 119.9ms and mean_after 339.8ms, explaining 99.89% of the series' variance.",
"fix": "Confirm with whoever owns this service whether a deploy, config change, or traffic shift landed on or just before 2026-01-21, and re-baseline latency alert thresholds against the new ~340ms level."}
],
"reconciliation": [
{"flag_id": "CHANGEPOINT_FOUND", "status": "confirmed", "note": "The mean genuinely shifted from ~120ms to ~340ms at 2026-01-21 and explains nearly all of the series' variance."},
{"flag_id": "VOLATILITY_SHIFT", "status": "confirmed", "note": "Day-to-day spread increased about 45x alongside the level shift, so post-2026-01-21 readings need wider tolerance bands."}
],
"next_lane": {"lane": "brief", "reason": "There is a concrete, well-explained changepoint plus an accompanying volatility increase - exactly the kind of signal an ops-monitoring reader would want turned into a decision."},
"body": {
"read": "The series is flat and tight for its first 20 days ... At 2026-01-21 it steps up sharply to a new level near 335-349ms and stays there ...",
"key_signals": [
{"kind": "changepoint", "label": "Single best regime split", "metric_name": "delta_abs / explained_ratio",
"metric_value": "+219.9ms (119.9 -> 339.8), explained_ratio 0.9989", "point": "2026-01-21",
"why": "One split at 2026-01-21 explains 99.89% of the series' variance."}
],
"data_quality_note": "The paste is clean: 30 complete daily rows, evenly spaced calendar dates, no missing values, duplicates, or invalid lines."
}
}
Every point field above is a timestamp label that appears either in prescan (as a changepoint.timestamp_label, an anomalies.top[].timestamp_label, or forecast.next_label) or literally in series_text. Anything else is rendered by the web app but marked point_ungrounded: true rather than trusted silently - the same check you should run if you parse this yourself.
7. Worked example: the brief task
The same series, lane switched to brief, purpose left at ops-monitoring, with a prior_diagnostics handoff carrying the diagnostics run's own verdict, key_signals and a trimmed findings list - exactly what the web app's handoff button sends:
{
"task": "brief",
"series_text": "2026-01-01,118\n...\n2026-01-30,342",
"value_label": "daily p95 API latency, in milliseconds",
"purpose": "ops-monitoring",
"prescan": { "...": "the same object as the diagnostics request above" },
"prior_diagnostics": {
"verdict": "shifting",
"key_signals": [ { "kind": "changepoint", "...": "..." } ],
"findings": [ {"id": "PD-001", "severity": "high", "point": "2026-01-21", "title": "Mean latency roughly tripled on 2026-01-21"} ]
}
}
Output body shape for task: "brief" (also a real captured response, trimmed):
{
"task": "brief", "task_inferred": false, "verdict": "shifting",
"findings": [
{"id": "PD-001", "severity": "high", "point": "2026-01-21", "title": "Monitoring baseline is now stale as of 2026-01-21", "why": "...", "fix": "Re-baseline p95 alert thresholds ..."}
],
"reconciliation": [
{"flag_id": "CHANGEPOINT_FOUND", "status": "confirmed", "note": "..."},
{"flag_id": "VOLATILITY_SHIFT", "status": "confirmed", "note": "..."}
],
"next_lane": {"lane": "", "reason": ""},
"body": {
"purpose": "ops-monitoring",
"executive_summary": "p95 API latency has been running roughly three times higher (about 340ms versus a ~120ms baseline) since 2026-01-21, and this is one clean regime change rather than a gradual climb. ...",
"priority_actions": [
{"rank": 1, "action": "Re-baseline p95 latency alert thresholds and dashboard reference lines to the post-2026-01-21 level (~340ms).",
"signal": "changepoint", "point": "2026-01-21", "rationale": "The changepoint places the whole regime split at 2026-01-21 (mean 119.9 to 339.8, explained_ratio 0.9989), matching prior diagnostics PD-001."}
],
"watch_next": ["Whether p95 stays flat near ~340ms over the next several days or keeps moving - only 10 points exist in the post-shift regime so far."],
"longer_term": ["If the 2026-01-21 change turns out to be deliberate or expected, update the documented SLO/baseline definition itself rather than continuing to treat the new level as an anomaly."]
}
}
next_lane is always {"lane": "", "reason": ""} from this task - there is no third lane. The house rules require treating prior_diagnostics.verdict as already established rather than recomputing a different one from the same prescan facts, which is why verdict above stays "shifting" across both lanes.