Schedule Desk — API

Sequence a board from your own code.

API tokens Open the app

Use the scheduler from your own code

Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS, so you can script it from any language: re-sequence a board every time MRP drops a new release, wire it into an MES export, or re-run the schedule when a machine goes down. This page walks through each call with examples in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#.

Basics

Base URL: https://api.skillsafe.ai/v1/app-api. Every request sends Authorization: Bearer <token> and JSON bodies with Content-Type: application/json. Responses are wrapped in an envelope: {"ok":true,"data":…} on success, {"error":{"code","message"}} on failure.

There is no /apps/schedule-desk/ path segment. The app is bound to the token itself, which is minted at POST /v1/app-api/guest with the slug — every later call is just /me, /estimate, /run or /run-stream.

CodeMeaningWhat to do
unauthorizedMissing, stale or revoked tokenMint a new guest token, or sign in for a personal one
not_foundThe route does not existCheck for a stray /apps/<slug>/ segment — that is the usual cause
payment_requiredBalance below the run minimumTop up; /estimate tells you the reserve and the minimum in advance
validation_errorBody is not the input objectSend the fields at the top level, not wrapped in {"input": …}
rate_limitedToo many requestsBack off and retry; do not tight-loop
/me, /estimate and /guest are free. /run and /run-stream spend credits.

The input this app sends

These are the exact fields the app's own run path submits.

FieldTypeNotes
datastring, requiredThe released work orders and the scenario as pasted. Job lines shaped like WO-4411: 6h, due 24 (processing hours first, then the due time in hours from now), plus prose about lines and work centers, attended hours, the changeover matrix, frozen windows, disruptions and rush orders. The app cuts the middle at 40,000 characters and keeps both ends, announcing the cut in-band; do the same rather than truncating the tail, because the floor notes and the explicit ask live at the end.
notesstring, optionalUp to 6,000 characters. Business context: frozen windows, quality holds, management priorities, what worries the scheduler. Also where the app folds in answers to a previous run's open questions.
taskstring, optionalOne of Full schedule, Bottleneck check, Changeover sequencing, Rush order insertion or Disruption response. Defaults to Full schedule. The narrower tasks still report a serious risk from any area; the task sets where the depth goes.
factsstring, optionalThe output of the browser-side mechanical scan: the job lines it detected with processing and due hours, total load, and the naive single-machine EDD and SPT lateness reads. Treated by the prompt as a hint to cross-check, never as a verdict — where the model's own reading of the paste disagrees, its reading wins. The app computes it in the browser; you can compute it yourself or omit it.
retry_notestring, optionalOnly used on the app's one automatic reformat retry: a verbatim restatement of the output shape, sent when the first reply failed to parse. Omit it on a first attempt.
Send an Idempotency-Key header on every run. The app derives it from a hash of the input plus the attempt number, so a dropped connection replays the same job instead of billing a second one.

The output contract

The reply is plain text — no JSON, no code fence around the whole response. A reply that breaks any of these rules is rejected by the app's parser, which then retries once.

FEASIBILITY: Executable | At risk - constraints noted | Not schedulable
BOTTLENECK: <the constraint line or work center, or exactly "Not identified">
CONFIDENCE: <integer 0-100, bare, no percent sign>
SUMMARY: <2-4 sentences, may wrap, ends at the first blank line>

## Constraint assessment
- …
## Job sequence
- …
## Changeover plan
- …
## Risks and watchouts
- …
## Recommended actions
- …
## Open questions
- …   (or the single bullet "- None.")
All four tag lines and all six headings are required, in that order. Every line inside a section is a - bullet; a bullet may wrap onto indented continuation lines. A section with nothing to report carries the single bullet - None.

1. Get a token

A guest token is one call and needs no account. For a personal token that bills your own credits, open the token page and sign in — it shows the token, copies it, and copies a ready-made shell export. You never need the browser developer console.

POST /v1/app-api/guest
TOKEN="${SKILLSAFE_TOKEN:-YOUR_TOKEN}"

# Or mint a guest token, no account needed:
TOKEN=$(curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
  -H "Content-Type: application/json" \
  -d '{"slug":"schedule-desk"}' | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["token"])')
echo "${TOKEN:0:8}..."
import json, urllib.request

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"   # or leave blank and mint a guest token below

def call(path, body=None, token=None, method=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(API + 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:
        return json.load(r)["data"]

if TOKEN == "YOUR_TOKEN":
    TOKEN = call("/guest", {"slug": "schedule-desk"})["token"]
print(TOKEN[:8] + "...")
const API = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN";   // or mint a guest token below

async function call(path, body, method) {
  const res = await fetch(API + path, {
    method: method || (body ? "POST" : "GET"),
    headers: {
      "Content-Type": "application/json",
      ...(TOKEN && TOKEN !== "YOUR_TOKEN" ? { Authorization: "Bearer " + TOKEN } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) throw new Error(json.error?.code + ": " + json.error?.message);
  return json.data;
}

if (TOKEN === "YOUR_TOKEN") {
  TOKEN = (await call("/guest", { slug: "schedule-desk" })).token;
}
console.log(TOKEN.slice(0, 8) + "...");
package main

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

const API = "https://api.skillsafe.ai/v1/app-api"

var token = "YOUR_TOKEN"

func call(path string, body any, out any) error {
    var buf *bytes.Buffer = bytes.NewBuffer(nil)
    method := "GET"
    if body != nil {
        method = "POST"
        b, _ := json.Marshal(body)
        buf = bytes.NewBuffer(b)
    }
    req, _ := http.NewRequest(method, API+path, buf)
    req.Header.Set("Content-Type", "application/json")
    if token != "YOUR_TOKEN" {
        req.Header.Set("Authorization", "Bearer "+token)
    }
    res, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer res.Body.Close()
    var env struct {
        Data json.RawMessage `json:"data"`
    }
    if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
        return err
    }
    return json.Unmarshal(env.Data, out)
}

func main() {
    var guest struct{ Token string `json:"token"` }
    if token == "YOUR_TOKEN" {
        _ = call("/guest", map[string]string{"slug": "schedule-desk"}, &guest)
        token = guest.Token
    }
    fmt.Println(token[:8] + "...")
}
import java.net.URI;
import java.net.http.*;

public class ScheduleDesk {
  static final String API = "https://api.skillsafe.ai/v1/app-api";
  static String token = "YOUR_TOKEN";
  static final HttpClient HTTP = HttpClient.newHttpClient();

  static String call(String path, String body) throws Exception {
    HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(API + path))
        .header("Content-Type", "application/json");
    if (!token.equals("YOUR_TOKEN")) b.header("Authorization", "Bearer " + token);
    b = body == null ? b.GET() : b.POST(HttpRequest.BodyPublishers.ofString(body));
    return HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString()).body();
  }

  public static void main(String[] args) throws Exception {
    if (token.equals("YOUR_TOKEN")) {
      String res = call("/guest", "{\"slug\":\"schedule-desk\"}");
      token = res.replaceAll(".*\"token\"\\s*:\\s*\"([^\"]+)\".*", "$1");
    }
    System.out.println(token.substring(0, 8) + "...");
  }
}
require "json"
require "net/http"

API = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = "YOUR_TOKEN"

def call(path, body = nil, token: nil, method: nil)
  uri = URI(API.to_s + path)
  req = if body || method == "POST"
          Net::HTTP::Post.new(uri)
        else
          Net::HTTP::Get.new(uri)
        end
  req["Content-Type"] = "application/json"
  req["Authorization"] = "Bearer #{token}" if token
  req.body = JSON.dump(body) if body
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  JSON.parse(res.body)["data"]
end

token = TOKEN == "YOUR_TOKEN" ? call("/guest", { slug: "schedule-desk" })["token"] : TOKEN
puts token[0, 8] + "..."
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$token = "YOUR_TOKEN";

function call(string $path, ?array $body = null, ?string $token = null, string $method = null): array {
    $headers = ["Content-Type: application/json"];
    if ($token) { $headers[] = "Authorization: Bearer $token"; }
    $ch = curl_init(API . $path);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    } elseif ($method) {
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    }
    $raw = curl_exec($ch);
    curl_close($ch);
    return json_decode($raw, true)["data"];
}

if ($token === "YOUR_TOKEN") {
    $token = call("/guest", ["slug" => "schedule-desk"])["token"];
}
echo substr($token, 0, 8) . "...\n";
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

const string API = "https://api.skillsafe.ai/v1/app-api";
string token = "YOUR_TOKEN";
var http = new HttpClient();

async Task<JsonElement> Call(string path, object? body = null, string? method = null) {
    var req = new HttpRequestMessage(
        body != null ? HttpMethod.Post : new HttpMethod(method ?? "GET"), API + path);
    if (body != null)
        req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
    if (token != "YOUR_TOKEN")
        req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
    var res = await http.SendAsync(req);
    var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
    return doc.RootElement.GetProperty("data");
}

if (token == "YOUR_TOKEN") {
    var guest = await Call("/guest", new { slug = "schedule-desk" });
    token = guest.GetProperty("token").GetString()!;
}
Console.WriteLine(token[..8] + "...");

2. Check who you are and what you can spend

Free. Returns subject_type (user or guest), subject_id and credits.

GET /v1/app-api/me
curl -s https://api.skillsafe.ai/v1/app-api/me \
  -H "Authorization: Bearer $TOKEN"
# {"ok":true,"data":{"subject_type":"guest","subject_id":"gst_...","credits":0}}
me = call("/me", token=TOKEN)
print(me["subject_type"], me["credits"])
const me = await call("/me");
console.log(me.subject_type, me.credits);
var me struct {
    SubjectType string `json:"subject_type"`
    Credits     int    `json:"credits"`
}
_ = call("/me", nil, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("/me", null));
me = call("/me", token: token)
puts "#{me['subject_type']} #{me['credits']}"
$me = call("/me", null, $token);
echo $me["subject_type"] . " " . $me["credits"] . "\n";
var me = await Call("/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");

3. Estimate before you spend

Free, creates no job and charges nothing. hold_credits is what gets reserved, not the price — the actual charge is usually far lower, because the hold prices the full output cap. If your balance sits between min_credits and hold_credits the run still executes with a reduced cap and comes back with truncated: true. Compare the balance from step 2 against hold_credits before you submit; a 402 after the fact is avoidable.

POST /v1/app-api/estimate
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"data":"Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...","notes":"The first 8h on Line 1 are frozen","task":"Full schedule","facts":""}'
# {"hold_credits":1578,"min_credits":149,"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000}
payload = {
    "data": "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
    "notes": "The first 8h on Line 1 are frozen",
    "task": "Full schedule",
    "facts": "",
}
est = call("/estimate", payload, token=TOKEN)
print(est["hold_credits"], est["min_credits"], est["model"])
const payload = {
  data: "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
  notes: "The first 8h on Line 1 are frozen",
  task: "Full schedule",
  facts: "",
};
const est = await call("/estimate", payload);
console.log(est.hold_credits, est.min_credits, est.model);
payload := map[string]string{
    "data":     "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
    "notes": "The first 8h on Line 1 are frozen",
    "task": "Full schedule",
    "facts":     "",
}
var est struct {
    HoldCredits int    `json:"hold_credits"`
    MinCredits  int    `json:"min_credits"`
    Model       string `json:"model"`
}
_ = call("/estimate", payload, &est)
fmt.Println(est.HoldCredits, est.MinCredits, est.Model)
String payload = "{\"data\":\"Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...\","
    + "\"notes\":\"The first 8h on Line 1 are frozen\",\"task\":\"Full schedule\",\"facts\":\"\"}";
System.out.println(call("/estimate", payload));
payload = {
  data: "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
  notes: "The first 8h on Line 1 are frozen",
  task: "Full schedule",
  facts: ""
}
est = call("/estimate", payload, token: token)
puts "#{est['hold_credits']} #{est['min_credits']} #{est['model']}"
$payload = [
    "data" => "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
    "notes" => "The first 8h on Line 1 are frozen",
    "task" => "Full schedule",
    "facts" => "",
];
$est = call("/estimate", $payload, $token);
echo "{$est['hold_credits']} {$est['min_credits']} {$est['model']}\n";
var payload = new {
    data = "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
    notes = "The first 8h on Line 1 are frozen",
    task = "Full schedule",
    facts = ""
};
var est = await Call("/estimate", payload);
Console.WriteLine($"{est.GetProperty("hold_credits")} {est.GetProperty("model")}");

4. Run it, and poll for the result

This spends credits. The body is the input object directly — not wrapped in {"input": …}. Send an Idempotency-Key: reuse the same key and the same job comes back instead of a second charge. /run returns a job_id; poll /jobs/{id} until status is succeeded or failed, then read output.output and parse it against the contract above.

POST /v1/app-api/run
GET /v1/app-api/jobs/{job_id}
KEY="schedule-desk-$(printf '%s' "$BOARD_TEXT" | shasum | cut -c1-8)-1"

JOB=$(curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d @payload.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')

until curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \
  -H "Authorization: Bearer $TOKEN" | grep -q '"status":"succeeded"'; do sleep 2; done

curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" -H "Authorization: Bearer $TOKEN" \
  | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["output"]["output"])'
import hashlib, time, urllib.request

key = "schedule-desk-" + hashlib.sha256(payload["data"].encode()).hexdigest()[:8] + "-1"

req = urllib.request.Request(API + "/run", data=json.dumps(payload).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
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("/jobs/" + job_id, token=TOKEN)
    if job["status"] in ("succeeded", "failed"):
        break
    time.sleep(2)

print(job["output"]["output"])
const enc = new TextEncoder().encode(payload.data);
const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", enc))]
  .map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 8);
const key = `schedule-desk-${digest}-1`;

const res = await fetch(API + "/run", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + TOKEN,
    "Idempotency-Key": key,
  },
  body: JSON.stringify(payload),
});
const { data } = await res.json();

let job;
do {
  await new Promise((r) => setTimeout(r, 2000));
  job = await call("/jobs/" + data.job_id);
} while (job.status !== "succeeded" && job.status !== "failed");

console.log(job.output.output);
b, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run", bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "schedule-desk-"+fingerprint+"-1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()

var env struct {
    Data struct {
        JobID string `json:"job_id"`
    } `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&env)

var job struct {
    Status string `json:"status"`
    Output struct {
        Output string `json:"output"`
    } `json:"output"`
}
for {
    _ = call("/jobs/"+env.Data.JobID, nil, &job)
    if job.Status == "succeeded" || job.Status == "failed" {
        break
    }
    time.Sleep(2 * time.Second)
}
fmt.Println(job.Output.Output)
HttpRequest run = HttpRequest.newBuilder(URI.create(API + "/run"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + token)
    .header("Idempotency-Key", "schedule-desk-" + fingerprint + "-1")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();
String created = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
String jobId = created.replaceAll(".*\"job_id\"\\s*:\\s*\"([^\"]+)\".*", "$1");

String job;
do {
  Thread.sleep(2000);
  job = call("/jobs/" + jobId, null);
} while (!job.contains("\"status\":\"succeeded\"") && !job.contains("\"status\":\"failed\""));
System.out.println(job);
require "digest"

key = "schedule-desk-#{Digest::SHA256.hexdigest(payload[:data])[0, 8]}-1"

uri = URI("#{API}/run")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}"
req["Idempotency-Key"] = key
req.body = JSON.dump(payload)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]

loop do
  job = call("/jobs/#{job_id}", token: token)
  if %w[succeeded failed].include?(job["status"])
    puts job.dig("output", "output")
    break
  end
  sleep 2
end
$key = "schedule-desk-" . substr(hash("sha256", $payload["data"]), 0, 8) . "-1";

$ch = curl_init(API . "/run");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Content-Type: application/json",
    "Authorization: Bearer $token",
    "Idempotency-Key: $key",
]);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);

do {
    sleep(2);
    $job = call("/jobs/$jobId", null, $token);
} while (!in_array($job["status"], ["succeeded", "failed"], true));

echo $job["output"]["output"] . "\n";
using System.Security.Cryptography;

var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload.data)))[..8].ToLower();
var runReq = new HttpRequestMessage(HttpMethod.Post, API + "/run") {
    Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
runReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
runReq.Headers.Add("Idempotency-Key", $"schedule-desk-{hash}-1");
var created = JsonDocument.Parse(await (await http.SendAsync(runReq)).Content.ReadAsStringAsync());
var jobId = created.RootElement.GetProperty("data").GetProperty("job_id").GetString();

JsonElement job;
do {
    await Task.Delay(2000);
    job = await Call("/jobs/" + jobId);
} while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"));

Console.WriteLine(job.GetProperty("output").GetProperty("output").GetString());

5. Stream it instead

This spends credits. /run-stream returns text/event-stream: delta events carry output as it generates and a final done event carries charged_credits, truncated and the authoritative full output. Prefer the done payload over the concatenated deltas — the stream can drop the tail. The same Idempotency-Key rule applies. In the browser, the vendored SDK wraps this: ss.runStream(input, { idempotencyKey, onDelta }).

POST /v1/app-api/run-stream
curl -N -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d @payload.json
# event: delta
# data: {"text":"FEASIBILITY: At risk - constraints noted\n"}
# ...
# event: done
# data: {"job_id":"job_...","charged_credits":412,"truncated":false,"output":{"output":"FEASIBILITY: ..."}}
req = urllib.request.Request(API + "/run-stream", data=json.dumps(payload).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)

full = ""
with urllib.request.urlopen(req) as stream:
    for raw in stream:
        line = raw.decode().strip()
        if line.startswith("data:"):
            evt = json.loads(line[5:].strip())
            if "text" in evt:
                full += evt["text"]
            elif "output" in evt:
                full = evt["output"]["output"]   # authoritative
print(full)
const res = await fetch(API + "/run-stream", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + TOKEN,
    "Idempotency-Key": key,
  },
  body: JSON.stringify(payload),
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", full = "";
for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  buf += decoder.decode(value, { stream: true });
  const lines = buf.split("\n");
  buf = lines.pop();
  for (const line of lines) {
    if (!line.startsWith("data:")) continue;
    const evt = JSON.parse(line.slice(5).trim());
    if (evt.text) full += evt.text;
    else if (evt.output) full = evt.output.output;   // authoritative
  }
}
console.log(full);
b, _ = json.Marshal(payload)
req, _ = http.NewRequest("POST", API+"/run-stream", bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", key)
res, _ = http.DefaultClient.Do(req)
defer res.Body.Close()

full := ""
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
    line := sc.Text()
    if !strings.HasPrefix(line, "data:") {
        continue
    }
    var evt struct {
        Text   string `json:"text"`
        Output *struct {
            Output string `json:"output"`
        } `json:"output"`
    }
    _ = json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &evt)
    if evt.Output != nil {
        full = evt.Output.Output
    } else {
        full += evt.Text
    }
}
fmt.Println(full)
HttpRequest stream = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + token)
    .header("Idempotency-Key", key)
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

StringBuilder full = new StringBuilder();
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body()
    .filter(l -> l.startsWith("data:"))
    .forEach(l -> full.append(l.substring(5).trim()).append("\n"));
System.out.println(full);   // parse each line as JSON; prefer the final done event
uri = URI("#{API}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}"
req["Idempotency-Key"] = key
req.body = JSON.dump(payload)

full = ""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      chunk.each_line do |line|
        next unless line.start_with?("data:")
        evt = JSON.parse(line[5..].strip) rescue next
        full = evt.dig("output", "output") || (full + evt.fetch("text", ""))
      end
    end
  end
end
puts full
$full = "";
$ch = curl_init(API . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Content-Type: application/json",
    "Authorization: Bearer $token",
    "Idempotency-Key: $key",
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$full) {
    foreach (explode("\n", $chunk) as $line) {
        if (strpos($line, "data:") !== 0) { continue; }
        $evt = json_decode(trim(substr($line, 5)), true);
        if (isset($evt["output"]["output"])) { $full = $evt["output"]["output"]; }
        elseif (isset($evt["text"])) { $full .= $evt["text"]; }
    }
    return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
echo $full . "\n";
var streamReq = new HttpRequestMessage(HttpMethod.Post, API + "/run-stream") {
    Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
streamReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
streamReq.Headers.Add("Idempotency-Key", key);

var resp = await http.SendAsync(streamReq, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await resp.Content.ReadAsStreamAsync());
var full = "";
while (await reader.ReadLineAsync() is string line) {
    if (!line.StartsWith("data:")) continue;
    var evt = JsonDocument.Parse(line[5..].Trim()).RootElement;
    if (evt.TryGetProperty("output", out var o))
        full = o.GetProperty("output").GetString()!;
    else if (evt.TryGetProperty("text", out var t))
        full += t.GetString();
}
Console.WriteLine(full);

6. Parse the reply

The four tag lines come first, then the six sections. A minimal, faithful parser: read FEASIBILITY, BOTTLENECK and CONFIDENCE from the leading lines, take SUMMARY up to the first blank line, then split on ## headings and read - bullets. Reject the reply rather than guessing if a tag or a heading is missing — that is what the app does, and it is why the retry exists.

# Reject anything that does not open with the contract:
head -1 reply.txt | grep -Eq '^FEASIBILITY: (Executable|At risk - constraints noted|Not schedulable)$' || echo "contract violated"
grep -c '^## ' reply.txt   # must be 6
import re

SECTIONS = ["Constraint assessment", "Job sequence", "Changeover plan",
            "Risks and watchouts", "Recommended actions", "Open questions"]

def parse(text):
    feas = re.search(r"^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$", text, re.M)
    drum = re.search(r"^BOTTLENECK:\s*(.+?)\s*$", text, re.M)
    con = re.search(r"^CONFIDENCE:\s*(\d{1,3})\s*$", text, re.M)
    summ = re.search(r"^SUMMARY:\s*(.*?)(?:\n\s*\n)", text, re.M | re.S)
    if not (feas and drum and con and summ):
        return None
    out = {"feasibility": feas.group(1), "bottleneck": drum.group(1),
           "confidence": int(con.group(1)), "summary": " ".join(summ.group(1).split()),
           "sections": {}}
    for name in SECTIONS:
        body = re.search(r"^##\s+" + re.escape(name) + r"\s*$(.*?)(?=^##\s|\Z)",
                         text, re.M | re.S)
        if body is None:
            return None
        bullets = [b.strip() for b in re.findall(r"^-\s+(.*)$", body.group(1), re.M)]
        out["sections"][name] = [] if bullets == ["None."] else bullets
    return out
const SECTIONS = ["Constraint assessment", "Job sequence", "Changeover plan",
                  "Risks and watchouts", "Recommended actions", "Open questions"];

function parse(text) {
  const feas = /^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$/m.exec(text);
  const drum = /^BOTTLENECK:\s*(.+?)\s*$/m.exec(text);
  const con = /^CONFIDENCE:\s*(\d{1,3})\s*$/m.exec(text);
  const summ = /^SUMMARY:\s*([\s\S]*?)\n\s*\n/m.exec(text);
  if (!feas || !drum || !con || !summ) return null;
  const sections = {};
  for (const name of SECTIONS) {
    const re = new RegExp("^##\\s+" + name + "\\s*$([\\s\\S]*?)(?=^##\\s|$)", "m");
    const body = re.exec(text);
    if (!body) return null;
    const bullets = [...body[1].matchAll(/^-\s+(.*)$/gm)].map((m) => m[1].trim());
    sections[name] = bullets.length === 1 && /^none\.?$/i.test(bullets[0]) ? [] : bullets;
  }
  return { feasibility: feas[1], bottleneck: drum[1], confidence: +con[1],
           summary: summ[1].replace(/\s+/g, " ").trim(), sections };
}
// Split on the six headings, then read "- " bullets out of each block.
sections := regexp.MustCompile(`(?m)^##\s+(.+)$`).Split(reply, -1)
names := regexp.MustCompile(`(?m)^##\s+(.+)$`).FindAllStringSubmatch(reply, -1)
if len(names) != 6 {
    return errors.New("contract violated: expected six sections")
}
for i, n := range names {
    bullets := regexp.MustCompile(`(?m)^-\s+(.*)$`).FindAllStringSubmatch(sections[i+1], -1)
    fmt.Println(n[1], len(bullets))
}
String[] names = {"Constraint assessment", "Job sequence", "Changeover plan",
                  "Risks and watchouts", "Recommended actions", "Open questions"};
for (String n : names) {
  if (!reply.contains("## " + n)) throw new IllegalStateException("missing section: " + n);
}
java.util.regex.Matcher m =
    java.util.regex.Pattern.compile("^FEASIBILITY:\\s*(Executable|At risk - constraints noted|Not schedulable)$",
        java.util.regex.Pattern.MULTILINE).matcher(reply);
if (!m.find()) throw new IllegalStateException("no FEASIBILITY line");
System.out.println(m.group(1));
SECTIONS = ["Constraint assessment", "Job sequence", "Changeover plan",
            "Risks and watchouts", "Recommended actions", "Open questions"].freeze

def parse(text)
  feas = text[/^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$/, 1]
  drum = text[/^BOTTLENECK:\s*(.+?)\s*$/, 1]
  con = text[/^CONFIDENCE:\s*(\d{1,3})\s*$/, 1]
  return nil unless feas && drum && con

  sections = SECTIONS.to_h do |name|
    body = text[/^##\s+#{Regexp.escape(name)}\s*$(.*?)(?=^##\s|\z)/m, 1]
    return nil unless body
    bullets = body.scan(/^-\s+(.*)$/).flatten.map(&:strip)
    [name, bullets == ["None."] ? [] : bullets]
  end
  { feasibility: feas, bottleneck: drum, confidence: con.to_i, sections: sections }
end
$sections = ["Constraint assessment", "Job sequence", "Changeover plan",
             "Risks and watchouts", "Recommended actions", "Open questions"];

preg_match('/^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$/m', $reply, $feas);
preg_match('/^BOTTLENECK:\s*(.+?)\s*$/m', $reply, $drum);
preg_match('/^CONFIDENCE:\s*(\d{1,3})\s*$/m', $reply, $con);
if (!$feas || !$drum || !$con) { throw new RuntimeException("contract violated"); }

$out = [];
foreach ($sections as $name) {
    $re = '/^##\s+' . preg_quote($name, '/') . '\s*$(.*?)(?=^##\s|\z)/ms';
    if (!preg_match($re, $reply, $body)) { throw new RuntimeException("missing $name"); }
    preg_match_all('/^-\s+(.*)$/m', $body[1], $bullets);
    $out[$name] = $bullets[1];
}
using System.Text.RegularExpressions;

string[] names = { "Constraint assessment", "Job sequence", "Changeover plan",
                   "Risks and watchouts", "Recommended actions", "Open questions" };

var feas = Regex.Match(reply, @"^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$", RegexOptions.Multiline);
if (!feas.Success) throw new InvalidOperationException("contract violated");

foreach (var name in names) {
    var body = Regex.Match(reply,
        $@"^##\s+{Regex.Escape(name)}\s*$(.*?)(?=^##\s|\z)",
        RegexOptions.Multiline | RegexOptions.Singleline);
    if (!body.Success) throw new InvalidOperationException($"missing section: {name}");
    var bullets = Regex.Matches(body.Groups[1].Value, @"^-\s+(.*)$", RegexOptions.Multiline);
    Console.WriteLine($"{name}: {bullets.Count}");
}
The feasibility value is one of exactly three phrases. Not schedulable is a real answer — it means the paste carried no usable work orders or capacity signal, and it is a result to act on rather than a failure to retry. When it is returned, Job sequence and Changeover plan are both - None. and Open questions says what to paste.