Day One — API

Build a dated onboarding plan from your HRIS, your ticket queue or a cron job.

API tokens Open the app

Plan a new hire's first ninety days from your own pipeline

Send the measured facts — the business-day calendar, every access item with its order-by date and overdue status, the roster, the Day 1 timetable and the week-1 1:1 slots — and get back one JSON object: role_tasks for that specific role, one_on_ones (exactly one topic per scheduled teammate), access_decisions (exactly one grant or not-needed per tool), goals at 30, 60 and 90 days each with a measure, a reading list, a sendable welcome_email, risks and unverified. The interesting part is that all of it is mechanically checkable, and the checker ships with the app: /rampkit.js is plain ES5 with no dependencies and no network calls, so your pipeline can compute the same facts and run the same reconciliation — both partitions counted, every returned date tested against the calendar, every named person looked up in the roster — before a plan ever reaches a manager. Every code step below is shown in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#; pick a language once and the whole page follows.

Basics

Base URL https://api.skillsafe.ai/v1/app-api, app slug day-one. Every request sends Authorization: Bearer <token> and JSON bodies with Content-Type: application/json. Responses are wrapped in an envelope: {"data": …} on success, {"error": {"code", "message"}} on failure. Plans are written by the gpt-terra model alias (currently gpt-5.6-terra) at a publisher markup of 1000 bps — 10%. Credits are units of 1/10 000 of a US dollar, so 10 000 credits is $1.00. /estimate, /me and /guest are free; /run and /run-stream are metered. Run input caps at 1 MB of JSON.

POST /v1/app-api/guest
GET /v1/app-api/me
POST /v1/app-api/estimate
POST /v1/app-api/run
GET /v1/app-api/jobs/{id}
POST /v1/app-api/run-stream
POST /v1/app-api/collections/plans/records
POST /v1/app-api/collections/plans/query
POST /v1/app-api/collections/plans/similar

Error codes

codestatuswhat it means
unauthorized401Missing or stale token. Mint a guest token or sign in again.
forbidden403The token belongs to a different app.
payment_required402Balance below min_credits. Call /estimate first and compare against /me.
validation_error400Malformed body. error.details names the field. A where value that is not an operator object lands here.
rate_limited429Back off. /similar is 30 req/min per IP, tighter than the other data endpoints.
not_found404Unknown job or record id.
internal5xxRetry with the SAME Idempotency-Key - it returns the original job instead of billing again.
None of the arithmetic needs an account. The whole free lane — parsing the four input blocks, the business-day calendar, the lead-time backfill and its overdue verdict, the dependency sort, the Day 1 packer, the 1:1 distribution, the buddy ranking, the plan document, the task CSV, the .ics calendar and every check — is in /rampkit.js. Load it in Node with a global.window = {} stub and RampKit.analyze({hire, roster, access, policy, depth, as_of}) gives you the same facts this API expects, from RampKit.factsForModel(analysis).

Step 1 · Get a token

Two ways in. /tokens.html shows the token this browser already holds and copies a shell export for it — you never need the DevTools console. Or mint a guest token from anywhere: a guest can call /me and the free /estimate, which is enough to verify the model binding, but a plan run needs a personal token so it bills your own wallet.

# Option A - take the token this browser already holds: open /tokens.html,
# press "Copy shell export", and paste the line it prints.
export SKILLSAFE_TOKEN="aut_xxxxxxxxxxxxxxxxxxxx"

# Option B - mint a guest token with no browser at all. A guest can call /me and
# the free /estimate, which is enough to verify the model binding; a plan run
# needs a personal token so it bills your own wallet.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
  -H 'Content-Type: application/json' \
  -d '{"slug":"day-one"}'
# => {"data":{"token":"aut_...","subject_type":"guest","credits":0}}
import json, urllib.request

BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "day-one"
TOKEN = "YOUR_TOKEN"          # from /tokens.html, or the guest() call below

def call(path, body=None, token=None, method=None):
    """The whole client. Every later step is one line on top of this."""
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(BASE + path, data=data,
                                method=method or ("POST" if data else "GET"))
    req.add_header("Content-Type", "application/json")
    if token:
        req.add_header("Authorization", "Bearer " + token)
    with urllib.request.urlopen(req) as r:
        payload = json.loads(r.read().decode())
    if "error" in payload:
        raise RuntimeError(payload["error"]["code"] + ": " + payload["error"]["message"])
    return payload["data"]

def guest():
    return call("/guest", {"slug": SLUG})["token"]

if TOKEN == "YOUR_TOKEN":
    TOKEN = guest()
print(TOKEN[:8] + "...")
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "day-one";
let TOKEN = "YOUR_TOKEN";       // from /tokens.html, or the guest() call below

async function call(path, body, opts = {}) {
  const res = await fetch(BASE + path, {
    method: opts.method || (body ? "POST" : "GET"),
    headers: {
      "Content-Type": "application/json",
      ...(opts.token ? { Authorization: "Bearer " + opts.token } : {}),
      ...(opts.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : {})
    },
    body: body ? JSON.stringify(body) : undefined
  });
  const payload = await res.json();
  if (payload.error) throw new Error(payload.error.code + ": " + payload.error.message);
  return payload.data;
}

const guest = () => call("/guest", { slug: SLUG }).then((d) => d.token);

if (TOKEN === "YOUR_TOKEN") TOKEN = await guest();
console.log(TOKEN.slice(0, 8) + "...");
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
)

const base = "https://api.skillsafe.ai/v1/app-api"
const slug = "day-one"

var token = "YOUR_TOKEN" // from /tokens.html, or guest() below

type envelope struct {
	Data  json.RawMessage `json:"data"`
	Error *struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}

func call(path string, body any, tok string, idem string) (json.RawMessage, error) {
	var rdr io.Reader
	method := "GET"
	if body != nil {
		b, _ := json.Marshal(body)
		rdr = bytes.NewReader(b)
		method = "POST"
	}
	req, _ := http.NewRequest(method, base+path, rdr)
	req.Header.Set("Content-Type", "application/json")
	if tok != "" {
		req.Header.Set("Authorization", "Bearer "+tok)
	}
	if idem != "" {
		req.Header.Set("Idempotency-Key", idem)
	}
	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, errors.New(env.Error.Code + ": " + env.Error.Message)
	}
	return env.Data, nil
}

func guest() (string, error) {
	data, err := call("/guest", map[string]string{"slug": slug}, "", "")
	if err != nil {
		return "", err
	}
	var out struct{ Token string `json:"token"` }
	err = json.Unmarshal(data, &out)
	return out.Token, err
}

func main() {
	if token == "YOUR_TOKEN" {
		t, err := guest()
		if err != nil {
			panic(err)
		}
		token = t
	}
	fmt.Println(token[:8] + "...")
}
import java.net.URI;
import java.net.http.*;
import java.util.Optional;

public class DayOne {
  static final String BASE = "https://api.skillsafe.ai/v1/app-api";
  static final String SLUG = "day-one";
  static String token = "YOUR_TOKEN";   // from /tokens.html, or guest() below

  static final HttpClient HTTP = HttpClient.newHttpClient();

  /** Returns the raw JSON body. Any JSON library will do for parsing;
   *  the envelope is {"data": ...} on success, {"error": {...}} on failure. */
  static String call(String path, String body, String tok, String idem) throws Exception {
    HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
        .header("Content-Type", "application/json");
    if (tok != null) b.header("Authorization", "Bearer " + tok);
    if (idem != null) b.header("Idempotency-Key", idem);
    b = body == null ? b.GET() : b.POST(HttpRequest.BodyPublishers.ofString(body));
    HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
    if (res.body().contains("\"error\"")) throw new RuntimeException(res.body());
    return res.body();
  }

  static String guest() throws Exception {
    String out = call("/guest", "{\"slug\":\"" + SLUG + "\"}", null, null);
    int at = out.indexOf("\"token\":\"") + 9;
    return out.substring(at, out.indexOf('"', at));
  }

  public static void main(String[] args) throws Exception {
    if (token.equals("YOUR_TOKEN")) token = guest();
    System.out.println(token.substring(0, 8) + "...");
  }
}
require "json"
require "net/http"

BASE = URI("https://api.skillsafe.ai/v1/app-api")
SLUG = "day-one"
TOKEN = "YOUR_TOKEN"          # from /tokens.html, or guest below

def call(path, body = nil, token: nil, idem: nil)
  uri = URI(BASE.to_s + path)
  req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
  req["Content-Type"] = "application/json"
  req["Authorization"] = "Bearer #{token}" if token
  req["Idempotency-Key"] = idem if idem
  req.body = JSON.dump(body) if body
  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"]}" if payload["error"]
  payload["data"]
end

def guest
  call("/guest", { "slug" => SLUG })["token"]
end

token = TOKEN == "YOUR_TOKEN" ? guest : TOKEN
puts token[0, 8] + "..."
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "day-one";
$token = "YOUR_TOKEN";        // from /tokens.html, or guest() below

function call(string $path, ?array $body = null, ?string $token = null, ?string $idem = null) {
    $headers = ["Content-Type: application/json"];
    if ($token) $headers[] = "Authorization: Bearer $token";
    if ($idem)  $headers[] = "Idempotency-Key: $idem";
    $ch = curl_init(BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_CUSTOMREQUEST => $body === null ? "GET" : "POST",
    ]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    $payload = json_decode(curl_exec($ch), true);
    curl_close($ch);
    if (isset($payload["error"])) {
        throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
    }
    return $payload["data"];
}

function guest(): string {
    return call("/guest", ["slug" => SLUG])["token"];
}

if ($token === "YOUR_TOKEN") $token = guest();
echo substr($token, 0, 8), "...\n";
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "day-one";
var token = "YOUR_TOKEN";     // from /tokens.html, or Guest() below

var http = new HttpClient();

async Task<JsonElement> Call(string path, object? body = null, string? tok = null, string? idem = null)
{
    var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, Base + path);
    if (body is not null)
        req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
    if (tok is not null) req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tok);
    if (idem is not null) req.Headers.Add("Idempotency-Key", idem);
    var res = await http.SendAsync(req);
    using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
    if (doc.RootElement.TryGetProperty("error", out var err))
        throw new Exception(err.GetProperty("code").GetString() + ": " + err.GetProperty("message").GetString());
    return doc.RootElement.GetProperty("data").Clone();
}

async Task<string> Guest() => (await Call("/guest", new { slug = Slug })).GetProperty("token").GetString()!;

if (token == "YOUR_TOKEN") token = await Guest();
Console.WriteLine(token[..8] + "...");

Step 2 · Check the session and the balance

/me is free and tells you whether the token is a personal one or a guest, and what the wallet holds. Compare it against min_credits from step 3 before submitting: a 402 after the fact is a failure of your client, not of the user.

curl -s https://api.skillsafe.ai/v1/app-api/me \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# => {"data":{"subject_type":"user","credits":48210,"app_slug":"day-one"}}
# credits are 1/10 000 of a US dollar, so 48210 is $4.8210.
me = call("/me", token=TOKEN)
print(me["subject_type"], me["credits"], "credits =",
      "${:.4f}".format(me["credits"] / 10000))
const me = await call("/me", null, { token: TOKEN });
console.log(me.subject_type, me.credits, "credits = $" + (me.credits / 10000).toFixed(4));
data, err := call("/me", nil, token, "")
if err != nil {
	panic(err)
}
var me struct {
	SubjectType string `json:"subject_type"`
	Credits     int    `json:"credits"`
}
json.Unmarshal(data, &me)
fmt.Printf("%s %d credits = $%.4f\n", me.SubjectType, me.Credits, float64(me.Credits)/10000)
String me = call("/me", null, token, null);
System.out.println(me);   // {"data":{"subject_type":"user","credits":48210,...}}
me = call("/me", token: token)
puts "#{me["subject_type"]} #{me["credits"]} credits = $#{"%.4f" % (me["credits"] / 10000.0)}"
$me = call("/me", null, $token);
printf("%s %d credits = $%.4f\n", $me["subject_type"], $me["credits"], $me["credits"] / 10000);
var me = await Call("/me", null, token);
var credits = me.GetProperty("credits").GetInt32();
Console.WriteLine($"{me.GetProperty("subject_type").GetString()} {credits} credits = ${credits / 10000.0:F4}");

Step 3 · Estimate, and assert the model binding

/estimate costs nothing and creates no job. It returns model, model_alias, markup_bps, hold_credits, min_credits and sponsor_enabled. hold_credits is reserved against the full output cap; the run settles at charged_credits, usually far lower. Because it is free, it is also the cheapest possible assertion in CI that this app is still bound to the model and markup you expect.

curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d @input.json
# => {"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000,
#             "hold_credits":3120,"min_credits":260,"sponsor_enabled":false}}
#
# hold_credits is RESERVED, not charged: it prices the full output cap. The run
# settles at charged_credits, usually far lower. /estimate is free and creates
# no job, so it is also the cheapest way to assert the model binding in CI.
est = call("/estimate", INPUT, token=TOKEN)
assert est["model"] == "gpt-5.6-terra" and est["model_alias"] == "gpt-terra"
assert est["markup_bps"] == 1000
if me["credits"] < est["min_credits"]:
    raise SystemExit("balance below the model minimum - top up before running")
print("reserved up to", est["hold_credits"], "credits; only what the run uses is charged")
const est = await call("/estimate", INPUT, { token: TOKEN });
if (est.model_alias !== "gpt-terra" || est.markup_bps !== 1000) throw new Error("unexpected binding");
if (me.credits < est.min_credits) throw new Error("balance below the model minimum");
console.log("reserved up to", est.hold_credits, "- charged is usually much less");
data, err = call("/estimate", input, token, "")
if err != nil {
	panic(err)
}
var est struct {
	Model       string `json:"model"`
	ModelAlias  string `json:"model_alias"`
	MarkupBps   int    `json:"markup_bps"`
	HoldCredits int    `json:"hold_credits"`
	MinCredits  int    `json:"min_credits"`
}
json.Unmarshal(data, &est)
if est.ModelAlias != "gpt-terra" || est.MarkupBps != 1000 {
	panic("unexpected model binding")
}
fmt.Println("reserved up to", est.HoldCredits, "credits")
String est = call("/estimate", INPUT_JSON, token, null);
if (!est.contains("\"model_alias\":\"gpt-terra\"")) throw new RuntimeException("unexpected binding");
System.out.println(est);
est = call("/estimate", INPUT, token: token)
raise "unexpected binding" unless est["model_alias"] == "gpt-terra" && est["markup_bps"] == 1000
raise "balance below the model minimum" if me["credits"] < est["min_credits"]
puts "reserved up to #{est["hold_credits"]} credits"
$est = call("/estimate", $input, $token);
if ($est["model_alias"] !== "gpt-terra" || $est["markup_bps"] !== 1000) {
    throw new RuntimeException("unexpected model binding");
}
if ($me["credits"] < $est["min_credits"]) {
    throw new RuntimeException("balance below the model minimum");
}
echo "reserved up to ", $est["hold_credits"], " credits\n";
var est = await Call("/estimate", input, token);
if (est.GetProperty("model_alias").GetString() != "gpt-terra") throw new Exception("unexpected binding");
if (credits < est.GetProperty("min_credits").GetInt32()) throw new Exception("balance below minimum");
Console.WriteLine($"reserved up to {est.GetProperty("hold_credits").GetInt32()} credits");

The input — what facts has to carry

Everything under facts is measured, not asked for. The model is instructed never to recompute a date: Day 1 through Day 5, the review dates, and every order-by date are already here, and anything it returns is checked against them. Compute this object with RampKit.factsForModel(RampKit.analyze(...)) from /rampkit.js, or assemble it yourself in the shape below.

{
  "depth": "light | standard | regulated",
  "note": "free-text steer (may be empty)",
  "facts": {
    "hire": { "name": "Priya Raman", "role": "Senior Backend Engineer", "level": "L5",
              "team": "Payments Platform", "manager": "Dana Whitfield",
              "location": "Remote (Berlin)", "tz": "", "employment": "Full-time",
              "role_family": "engineering", "workday": "09:00-17:30", "extra": [] },
    "as_of": "2026-08-17",
    "start_date": "2026-08-24",
    "start_weekday": "Monday",
    "start_assumed": false,
    "business_days": [{ "n": 1, "iso": "2026-08-24", "weekday": "Monday" }],
    "week1_end": "2026-08-31",
    "milestones": [{ "label": "30-day", "review_on": "2026-09-23",
                     "weekday": "Wednesday", "rolled": false }],
    "holidays": [{ "date": "2026-08-28", "label": "Summer company holiday" }],
    "trainings": ["Security awareness", "Code of conduct"],
    "policy_notes": [],
    "roster": [{ "id": "p2", "name": "Marco Silva", "role": "Staff Engineer",
                 "team": "Payments Platform", "functions": [], "is_manager": false }],
    "one_on_ones_scheduled": [{ "person_id": "p2", "name": "Marco Silva",
                                "role": "Staff Engineer", "day": 2,
                                "date": "2026-08-25", "time": "10:00-10:30" }],
    "one_on_ones_unplaced": [{ "person_id": "p9", "name": "Rui Alves",
                               "reason": "week 1 holds 4 days x 3 slots = 12" }],
    "buddy": { "person_id": "p3", "name": "Aiko Tanaka", "why": ["same team"],
               "named_by_user": false },
    "buddy_candidates": [{ "person_id": "p2", "name": "Marco Silva", "score": 5, "why": [] }],
    "access_source": "pasted | catalogue",
    "access": [{ "id": "t1", "tool": "Laptop and peripherals", "level": "hardware",
                 "owner": "IT", "lead_days": 10, "lead_assumed": false,
                 "order_by": "2026-08-10", "status": "overdue",
                 "slack_business_days": -5, "requires": [] }],
    "prestart_tasks": [{ "id": "s1", "task": "Send the welcome email",
                         "owner": "Dana Whitfield", "due": "2026-08-17",
                         "status": "due-soon", "slack_business_days": 0 }],
    "day1": { "date": "2026-08-24", "meeting_minutes": 255, "cap": 300,
              "slots": [{ "time": "09:00-09:45", "activity": "Welcome and orientation",
                          "with": "Dana Whitfield" }],
              "moved_to_day2": [] },
    "readiness": { "level": "ready | at-risk | blocked", "why": ["..."] },
    "input_gaps": [{ "id": "no_hr", "gap": "no HR contact", "detail": "..." }],
    "counts": { "roster": 6, "access": 12, "overdue": 1, "due_soon": 0, "ones": 3,
                "ones_unplaced": 0, "day1_slots": 7, "day1_overflow": 0,
                "trainings": 3, "holidays": 1, "gaps": 0 }
  },
  "hire_block": "the hire details as pasted",
  "roster_excerpt": "the roster, cut on whole rows with the header kept",
  "access_excerpt": "the access list, cut the same way",
  "policy_excerpt": "the policy box",
  "current_datetime": "2026-08-17T09:00:00+02:00 (Monday)"
}
Two fields do quiet work. as_of is what overdue and slack_business_days are measured from, so it is what makes "already late" a fact rather than an opinion - pin it in tests and the output stops drifting. input_gaps is the honest record of what the operator did not supply: when it contains no_hr, a task owned by the generic HR is correct and is not counted as an invented person.

Step 4 · Run and poll

Always send an Idempotency-Key: a content hash of the input plus an attempt counter. A timeout, a dropped connection or a 5xx retried with the same key returns the original job rather than billing a second one. output.output is the plan JSON as a string - parse it, then check it. truncated: true means a low balance cut the reply short; render it as partial rather than presenting it as a whole plan.

# The Idempotency-Key is a content hash of the input plus an attempt counter.
# Reusing it after a timeout or a 5xx returns the ORIGINAL job instead of
# billing a second one.
JOB=$(curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: day-one:9f2ac41b:a1' \
  -d @input.json | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')

# Poll to a terminal state.
until curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" | tee /tmp/job.json \
  | grep -q '"status":"\(succeeded\|failed\)"'; do sleep 2; done

# output.output is the plan JSON as a string. Parse it, then check it.
python3 -c 'import json;d=json.load(open("/tmp/job.json"))["data"];\
print(json.loads(d["output"]["output"])["title"]);\
print("charged", d.get("charged_credits"), "truncated", d.get("truncated"))'
import time, hashlib

key = "day-one:" + hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:8] + ":a1"
job_id = call("/run", INPUT, token=TOKEN)["job_id"]   # send the key as Idempotency-Key

while True:
    job = call("/jobs/" + job_id, token=TOKEN)
    if job["status"] in ("succeeded", "failed"):
        break
    time.sleep(2)

plan = json.loads(job["output"]["output"])            # the JSON object SKILL.md defines
if job.get("truncated"):
    print("reply cut short by the available balance - treat it as partial")
print(plan["title"], "-", len(plan["role_tasks"]), "role tasks,",
      len(plan["access_decisions"]), "access decisions")
print("charged", job.get("charged_credits"), "credits")
const key = "day-one:" + hash(JSON.stringify(INPUT)) + ":a1";  // any stable hash
const { job_id } = await call("/run", INPUT, { token: TOKEN, idempotencyKey: key });

let job;
for (;;) {
  job = await call("/jobs/" + job_id, null, { token: TOKEN });
  if (job.status === "succeeded" || job.status === "failed") break;
  await new Promise((r) => setTimeout(r, 2000));
}

const plan = JSON.parse(job.output.output);
if (job.truncated) console.warn("reply cut short by the balance - partial");
console.log(plan.title, plan.role_tasks.length, "tasks,", plan.access_decisions.length, "decisions");
data, err = call("/run", input, token, "day-one:9f2ac41b:a1")
if err != nil {
	panic(err)
}
var started struct{ JobID string `json:"job_id"` }
json.Unmarshal(data, &started)

var job struct {
	Status         string `json:"status"`
	Truncated      bool   `json:"truncated"`
	ChargedCredits int    `json:"charged_credits"`
	Output         struct{ Output string `json:"output"` } `json:"output"`
}
for {
	data, err = call("/jobs/"+started.JobID, nil, token, "")
	if err != nil {
		panic(err)
	}
	json.Unmarshal(data, &job)
	if job.Status == "succeeded" || job.Status == "failed" {
		break
	}
	time.Sleep(2 * time.Second)
}
var plan map[string]any
json.Unmarshal([]byte(job.Output.Output), &plan)
fmt.Println(plan["title"], "charged", job.ChargedCredits, "truncated", job.Truncated)
String started = call("/run", INPUT_JSON, token, "day-one:9f2ac41b:a1");
String jobId = started.split("\"job_id\":\"")[1].split("\"")[0];

String job;
while (true) {
  job = call("/jobs/" + jobId, null, token, null);
  if (job.contains("\"status\":\"succeeded\"") || job.contains("\"status\":\"failed\"")) break;
  Thread.sleep(2000);
}
System.out.println(job);   // data.output.output holds the plan JSON as a string
started = call("/run", INPUT, token: token, idem: "day-one:9f2ac41b:a1")
job = nil
loop do
  job = call("/jobs/#{started["job_id"]}", token: token)
  break if %w[succeeded failed].include?(job["status"])
  sleep 2
end

plan = JSON.parse(job["output"]["output"])
warn "reply cut short by the balance - partial" if job["truncated"]
puts "#{plan["title"]}: #{plan["role_tasks"].length} tasks, charged #{job["charged_credits"]}"
$started = call("/run", $input, $token, "day-one:9f2ac41b:a1");
do {
    $job = call("/jobs/" . $started["job_id"], null, $token);
    if (in_array($job["status"], ["succeeded", "failed"], true)) break;
    sleep(2);
} while (true);

$plan = json_decode($job["output"]["output"], true);
if (!empty($job["truncated"])) fwrite(STDERR, "reply cut short - partial\n");
printf("%s: %d tasks, charged %d\n", $plan["title"], count($plan["role_tasks"]), $job["charged_credits"]);
var started = await Call("/run", input, token, "day-one:9f2ac41b:a1");
var jobId = started.GetProperty("job_id").GetString();

JsonElement job;
while (true)
{
    job = await Call("/jobs/" + jobId, null, token);
    var status = job.GetProperty("status").GetString();
    if (status is "succeeded" or "failed") break;
    await Task.Delay(2000);
}

using var plan = JsonDocument.Parse(job.GetProperty("output").GetProperty("output").GetString()!);
Console.WriteLine(plan.RootElement.GetProperty("title").GetString());

The output contract

Exactly one JSON object, no prose and no code fences. Two of these fields are partitions, and that is the part worth wiring into your own tests: one_on_ones carries exactly one entry per scheduled person_id, and access_decisions exactly one per access id. Count them - an id appearing twice is as much a failure as one missing, and asserting presence alone will not catch it.

{
  "title": "Onboarding plan: Priya Raman, Senior Backend Engineer, Payments Platform",
  "readiness_note": "One or two sentences agreeing with facts.readiness and saying what to do about anything overdue.",
  "role_tasks": [
    { "phase": "pre-start | day1 | week1 | 30-day | 60-day | 90-day",
      "title": "Request a loaner laptop from IT",
      "owner": "IT",
      "due": "2026-08-19",
      "day": 3,
      "why": "the ordered machine cannot arrive by the start date" }
  ],
  "one_on_ones": [
    { "person_id": "p2", "name": "Marco Silva",
      "topic": "how the settlement retry path actually behaves" }
  ],
  "access_decisions": [
    { "tool_id": "t1", "decision": "grant | not-needed", "level": "hardware",
      "why": "cannot start without it" }
  ],
  "goals": {
    "d30": [{ "goal": "Ship one small change to the ledger service",
              "measure": "a merged pull request running in production" }],
    "d60": [{ "goal": "...", "measure": "..." }],
    "d90": [{ "goal": "...", "measure": "..." }]
  },
  "reading": [{ "title": "Payments architecture overview", "why": "the system she joins" }],
  "welcome_email": "markdown string, addressed by name, with the first working day",
  "risks": ["The laptop order is five business days past its order-by date."],
  "unverified": ["anything that could not be traced to facts - ideally empty"]
}

What the app checks, and what you should check too

checkhow it fails
one_on_ones partitionA scheduled teammate with no topic, a teammate named twice, a topic for someone in one_on_ones_unplaced, or a person_id not in facts.roster.
access_decisions partitionA tool with no decision, a tool decided twice, a tool_id that is not in facts.access, or a decision outside grant / not-needed.
date legalityAny due on a weekend or a listed holiday, a pre-start due after start_date, or a week-1 day outside 1–5. The claim is reported as sent, with the next working day beside it - nothing is silently corrected before the check runs.
people groundingA role_tasks[].owner who is neither in facts.roster nor the manager nor the hire nor one of the generic service roles.
goal measurabilityAn empty horizon fails; a goal with no measure warns.
overdue honestyWhen facts.counts.overdue is above zero and no entry in risks names one of those items.
document checksRun on the rendered plan: a missing section, a heading with nothing under it, a surviving [Name] placeholder, an ISO date in the document that is not a working day, a measured tool or scheduled teammate the document never names, or overdue work the document does not mention.
One exemption is deliberate and worth copying: when the start date the operator gave is itself a weekend or a holiday, that one ISO string is exempt from the working-day check, because the engine already reported it as a gap and rolled Day 1 forward. The exempt set is built before the scan from the engine's own measurement, so the two buckets are disjoint by construction - a genuinely wrong date cannot hide inside the exemption.

Step 5 · Stream it instead

/run-stream is the same call over server-sent events, which is what the app itself uses so the progress card can advance on real signals. The frame name arrives on the event: line - there is no type field inside the payload. Frames are job, delta, done and error. Accumulate the delta text and prefer done.output.output when it arrives; if the stream dies mid-flight, what you accumulated is usually still worth parsing.

# SSE. The frame name arrives on the `event:` line - there is no `type` field
# inside the payload. Frames: job (job_id), delta (text chunks), done (the
# terminal job with charged_credits), error.
curl -N -s -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: day-one:9f2ac41b:a1' \
  -d @input.json
# event: job
# data: {"job_id":"job_..."}
# event: delta
# data: {"text":"{\"title\":\"Onboarding plan"}
# event: done
# data: {"status":"succeeded","charged_credits":1180,"output":{"output":"{...}"}}
req = urllib.request.Request(BASE + "/run-stream", data=json.dumps(INPUT).encode(),
                             method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", "day-one:9f2ac41b:a1")

raw, event = "", None
with urllib.request.urlopen(req) as r:
    for line in r:
        line = line.decode().rstrip("\n")
        if line.startswith("event: "):
            event = line[7:]                     # the frame name is on this line
        elif line.startswith("data: "):
            payload = json.loads(line[6:])
            if event == "delta":
                raw += payload.get("text", "")   # stream the plan as it is written
            elif event == "done":
                raw = payload["output"]["output"] or raw
                print("charged", payload.get("charged_credits"))

plan = json.loads(raw)
const res = await fetch(BASE + "/run-stream", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + TOKEN,
    "Idempotency-Key": "day-one:9f2ac41b:a1"
  },
  body: JSON.stringify(INPUT)
});

const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null;
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += dec.decode(value, { stream: true });
  const lines = buf.split("\n");
  buf = lines.pop();
  for (const line of lines) {
    if (line.startsWith("event: ")) event = line.slice(7).trim();
    else if (line.startsWith("data: ")) {
      const payload = JSON.parse(line.slice(6));
      if (event === "delta") raw += payload.text || "";
      else if (event === "done") raw = payload.output?.output || raw;
    }
  }
}
const plan = JSON.parse(raw);
body, _ := json.Marshal(input)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "day-one:9f2ac41b:a1")
res, err := http.DefaultClient.Do(req)
if err != nil {
	panic(err)
}
defer res.Body.Close()

sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
var raw, event string
for sc.Scan() {
	line := sc.Text()
	switch {
	case strings.HasPrefix(line, "event: "):
		event = strings.TrimSpace(line[7:])
	case strings.HasPrefix(line, "data: "):
		var p struct {
			Text   string `json:"text"`
			Output struct{ Output string `json:"output"` } `json:"output"`
		}
		json.Unmarshal([]byte(line[6:]), &p)
		if event == "delta" {
			raw += p.Text
		} else if event == "done" && p.Output.Output != "" {
			raw = p.Output.Output
		}
	}
}
fmt.Println(len(raw), "characters of plan JSON")
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + token)
    .header("Idempotency-Key", "day-one:9f2ac41b:a1")
    .POST(HttpRequest.BodyPublishers.ofString(INPUT_JSON))
    .build();

StringBuilder raw = new StringBuilder();
String[] event = { null };
HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
  if (line.startsWith("event: ")) event[0] = line.substring(7).trim();
  else if (line.startsWith("data: ") && "delta".equals(event[0])) {
    String d = line.substring(6);
    int at = d.indexOf("\"text\":\"");
    if (at >= 0) raw.append(d, at + 8, d.lastIndexOf('"'));
  }
});
System.out.println(raw.length() + " characters streamed");
uri = URI(BASE.to_s + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}"
req["Idempotency-Key"] = "day-one:9f2ac41b:a1"
req.body = JSON.dump(INPUT)

raw = ""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      chunk.each_line do |line|
        line = line.chomp
        if line.start_with?("event: ")
          event = line[7..].strip
        elsif line.start_with?("data: ")
          payload = JSON.parse(line[6..])
          raw += payload["text"].to_s if event == "delta"
          raw = payload.dig("output", "output") || raw if event == "done"
        end
      end
    end
  end
end
plan = JSON.parse(raw)
$raw = "";
$event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($input),
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer $token",
        "Idempotency-Key: day-one:9f2ac41b:a1",
    ],
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
        foreach (explode("\n", $chunk) as $line) {
            $line = rtrim($line);
            if (str_starts_with($line, "event: ")) {
                $event = trim(substr($line, 7));
            } elseif (str_starts_with($line, "data: ")) {
                $payload = json_decode(substr($line, 6), true);
                if ($event === "delta") $raw .= $payload["text"] ?? "";
                if ($event === "done")  $raw = $payload["output"]["output"] ?? $raw;
            }
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);
$plan = json_decode($raw, true);
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream")
{
    Content = new StringContent(JsonSerializer.Serialize(input), Encoding.UTF8, "application/json")
};
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
req.Headers.Add("Idempotency-Key", "day-one:9f2ac41b:a1");

using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string? evt = null;
while (await reader.ReadLineAsync() is string line)
{
    if (line.StartsWith("event: ")) evt = line[7..].Trim();
    else if (line.StartsWith("data: "))
    {
        using var d = JsonDocument.Parse(line[6..]);
        if (evt == "delta" && d.RootElement.TryGetProperty("text", out var t))
            raw.Append(t.GetString());
        else if (evt == "done" && d.RootElement.TryGetProperty("output", out var o))
            raw.Clear().Append(o.GetProperty("output").GetString());
    }
}
using var plan = JsonDocument.Parse(raw.ToString());

Step 6 · Store and search past plans

The app declares one collection, plans, with acl_read: owner and acl_write: user - records belong to the calling identity. Declared fields are title, hire_role, team, summary, readiness, start_date (all strings), task_count and check_fails (numbers) and ran_at (timestamp); the rest of the document, including the whole plan markdown, round-trips intact but is not filterable. The embed set is title, hire_role, team and summary - the summary is the one that earns its place, because "the one where the laptop was late" lives in the readiness reason, not the title.

# Exact filter: every plan that came back blocked, newest first. `where` values
# must be operator OBJECTS - a bare value is rejected. Ordering is the `sort`
# object; `order_by` is silently ignored.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/plans/query \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"where":{"readiness":{"eq":"blocked"}},
       "sort":{"field":"ran_at","dir":"desc"},"limit":20}'

# Create a record. Note the path: /records, not the collection root.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/plans/records \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Priya Raman - Senior Backend Engineer","hire_role":"Senior Backend Engineer",
       "team":"Payments Platform","summary":"joining Payments on 2026-08-24; readiness ready",
       "readiness":"ready","start_date":"2026-08-24","task_count":34,"check_fails":0,
       "ran_at":"2026-08-17T09:00:00Z","plan_md":"# Onboarding plan..."}'

# Semantic search over title, hire_role, team and summary. 30 req/min per IP and
# about ten times the cost of the filter above - use `where` when an exact match
# would do.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/plans/similar \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"text":"the one where the laptop was late","limit":8}'
# Exact filter - cheap, and the right tool whenever the question has an exact answer.
page = call("/collections/plans/query", {
    "where": {"readiness": {"eq": "blocked"}},
    "sort": {"field": "ran_at", "dir": "desc"},
    "limit": 20
}, token=TOKEN)
for rec in page["records"]:
    print(rec["record_id"], rec["doc"]["title"], rec["doc"]["start_date"])

# Write one. The path ends in /records.
call("/collections/plans/records", {
    "title": "Priya Raman - Senior Backend Engineer",
    "hire_role": "Senior Backend Engineer",
    "team": "Payments Platform",
    "summary": "joining Payments Platform on 2026-08-24; readiness ready",
    "readiness": "ready", "start_date": "2026-08-24",
    "task_count": 34, "check_fails": 0,
    "ran_at": "2026-08-17T09:00:00Z", "plan_md": plan_markdown
}, token=TOKEN)

# Semantic search. Returns records with a `score`; debounce it, 30/min per IP.
hits = call("/collections/plans/similar",
            {"text": "the one where the laptop was late", "limit": 8}, token=TOKEN)
for rec in (hits if isinstance(hits, list) else hits["records"]):
    print(round(rec["score"], 3), rec["doc"]["summary"])
const page = await call("/collections/plans/query", {
  where: { readiness: { eq: "blocked" } },
  sort: { field: "ran_at", dir: "desc" },
  limit: 20
}, { token: TOKEN });

await call("/collections/plans/records", {
  title: "Priya Raman - Senior Backend Engineer",
  hire_role: "Senior Backend Engineer",
  team: "Payments Platform",
  summary: "joining Payments Platform on 2026-08-24; readiness ready",
  readiness: "ready", start_date: "2026-08-24",
  task_count: 34, check_fails: 0,
  ran_at: new Date().toISOString(), plan_md: planMarkdown
}, { token: TOKEN });

// similar() over the SDK resolves to the record ARRAY; the REST call returns
// {records}. Accept either shape rather than trusting one.
const hits = await call("/collections/plans/similar",
  { text: "the one where the laptop was late", limit: 8 }, { token: TOKEN });
for (const rec of Array.isArray(hits) ? hits : hits.records) {
  console.log(rec.score.toFixed(3), rec.doc.summary);
}
query := map[string]any{
	"where": map[string]any{"readiness": map[string]any{"eq": "blocked"}},
	"sort":  map[string]any{"field": "ran_at", "dir": "desc"},
	"limit": 20,
}
data, err = call("/collections/plans/query", query, token, "")
if err != nil {
	panic(err)
}
var page struct {
	Records []struct {
		RecordID string         `json:"record_id"`
		Doc      map[string]any `json:"doc"`
	} `json:"records"`
}
json.Unmarshal(data, &page)
for _, r := range page.Records {
	fmt.Println(r.RecordID, r.Doc["title"])
}

// Semantic search - note the /similar path and the 30 req/min per-IP limit.
data, _ = call("/collections/plans/similar",
	map[string]any{"text": "the one where the laptop was late", "limit": 8}, token, "")
fmt.Println(string(data))
String body = "{\"where\":{\"readiness\":{\"eq\":\"blocked\"}},"
    + "\"sort\":{\"field\":\"ran_at\",\"dir\":\"desc\"},\"limit\":20}";
System.out.println(call("/collections/plans/query", body, token, null));

// Create: the path ends in /records, not at the collection root.
String rec = "{\"title\":\"Priya Raman - Senior Backend Engineer\","
    + "\"hire_role\":\"Senior Backend Engineer\",\"team\":\"Payments Platform\","
    + "\"summary\":\"joining Payments Platform on 2026-08-24; readiness ready\","
    + "\"readiness\":\"ready\",\"start_date\":\"2026-08-24\",\"task_count\":34,"
    + "\"check_fails\":0,\"ran_at\":\"2026-08-17T09:00:00Z\"}";
call("/collections/plans/records", rec, token, null);

System.out.println(call("/collections/plans/similar",
    "{\"text\":\"the one where the laptop was late\",\"limit\":8}", token, null));
page = call("/collections/plans/query", {
  "where" => { "readiness" => { "eq" => "blocked" } },
  "sort" => { "field" => "ran_at", "dir" => "desc" },
  "limit" => 20
}, token: token)
page["records"].each { |r| puts "#{r["record_id"]} #{r["doc"]["title"]}" }

call("/collections/plans/records", {
  "title" => "Priya Raman - Senior Backend Engineer",
  "hire_role" => "Senior Backend Engineer",
  "team" => "Payments Platform",
  "summary" => "joining Payments Platform on 2026-08-24; readiness ready",
  "readiness" => "ready", "start_date" => "2026-08-24",
  "task_count" => 34, "check_fails" => 0,
  "ran_at" => Time.now.utc.iso8601, "plan_md" => plan_markdown
}, token: token)

hits = call("/collections/plans/similar",
            { "text" => "the one where the laptop was late", "limit" => 8 }, token: token)
records = hits.is_a?(Array) ? hits : hits["records"]
records.each { |r| puts "#{r["score"].round(3)} #{r["doc"]["summary"]}" }
$page = call("/collections/plans/query", [
    "where" => ["readiness" => ["eq" => "blocked"]],
    "sort" => ["field" => "ran_at", "dir" => "desc"],
    "limit" => 20,
], $token);
foreach ($page["records"] as $rec) {
    echo $rec["record_id"], " ", $rec["doc"]["title"], "\n";
}

call("/collections/plans/records", [
    "title" => "Priya Raman - Senior Backend Engineer",
    "hire_role" => "Senior Backend Engineer",
    "team" => "Payments Platform",
    "summary" => "joining Payments Platform on 2026-08-24; readiness ready",
    "readiness" => "ready", "start_date" => "2026-08-24",
    "task_count" => 34, "check_fails" => 0,
    "ran_at" => gmdate("c"), "plan_md" => $planMarkdown,
], $token);

$hits = call("/collections/plans/similar",
    ["text" => "the one where the laptop was late", "limit" => 8], $token);
foreach ($hits["records"] ?? $hits as $rec) {
    printf("%.3f %s\n", $rec["score"], $rec["doc"]["summary"]);
}
var page = await Call("/collections/plans/query", new
{
    where = new { readiness = new { eq = "blocked" } },
    sort = new { field = "ran_at", dir = "desc" },
    limit = 20
}, token);
foreach (var rec in page.GetProperty("records").EnumerateArray())
    Console.WriteLine(rec.GetProperty("doc").GetProperty("title").GetString());

await Call("/collections/plans/records", new
{
    title = "Priya Raman - Senior Backend Engineer",
    hire_role = "Senior Backend Engineer",
    team = "Payments Platform",
    summary = "joining Payments Platform on 2026-08-24; readiness ready",
    readiness = "ready", start_date = "2026-08-24",
    task_count = 34, check_fails = 0,
    ran_at = DateTime.UtcNow.ToString("o"), plan_md = planMarkdown
}, token);

var hits = await Call("/collections/plans/similar",
    new { text = "the one where the laptop was late", limit = 8 }, token);
Console.WriteLine(hits.ToString());

Three traps, all verified live. Every where entry must be an operator object - {"readiness":"blocked"} is rejected, {"readiness":{"eq":"blocked"}} is right. Ordering is the sort object; order_by is accepted and then silently ignored, leaving you with created_at desc. And record creation posts to /collections/plans/records, not to the collection root.

Data endpoints share 120 requests/min; /collections/{name}/similar is 30/min per IP and costs roughly an order of magnitude more than a where filter - use the filter whenever an exact match would do, and never fire a similarity query per keystroke. Vector indexing is asynchronous, so a similar call immediately after a write can lag by seconds. There is no backfill: records written before an embed field existed are never vectorized. Storage quotas that matter here: 64 KB per document, 10 000 records per collection, 1 000 records per owner.