Drive Slack Desk from your own code
Everything the page does is available over HTTP. The natural uses are a CI job that lints every Slack message template in a repository before release, a bot that turns a changelog entry into a Block Kit payload, and a review step that refuses to post a payload the validator rejects.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses the same
envelope, so one helper covers the whole API:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
Send your app slug as X-App-Slug: slack-desk and your token as
Authorization: Bearer … on every call.
Error codes
| code | status | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Get a new one from the token page. |
payment_required | 402 | The balance is below min_credits. Call /estimate first and top up. |
forbidden | 403 | The token is valid but not for this app, or a guest token tried a metered run. Check the X-App-Slug header. |
not_found | 404 | Unknown job id, unknown collection, or the app slug does not exist. |
conflict | 409 | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
validation_error | 422 | A required field is missing or the wrong type. A body that is not valid JSON at all comes back as a 400. |
rate_limited | 429 | Too many requests. Back off and retry; do not tight-loop. |
internal | 5xx | A server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice. |
The task field comes first
Slack Desk is one app with four lanes over one work object. Every request must carry a
task; it selects the lane, and it changes both the cost and the shape of the
output. If task is missing the model picks the closest lane and names its choice in
## SUMMARY — useful as a fallback, useless as a contract. Send it.
task | stage | what you send | what you get back |
|---|---|---|---|
review | inspect | a message, or a payload | LANE, SUMMARY, VERDICT, FINDINGS, REWRITE, CHECKS |
compose | produce | raw notes or a draft | LANE, SUMMARY, MESSAGE, SHORT, THREAD, WHY, CHECKS |
blocks | produce | the message text | LANE, SUMMARY, PAYLOAD, FALLBACK, WHY, CHECKS |
ship | deliver | the payload | LANE, SUMMARY, PAYLOAD, PYTHON, JAVASCRIPT, WEBHOOK, HANDLER, CHECKS |
The input object
Taken from the app's own buildInput(), not from intent:
| field | type | lanes | meaning |
|---|---|---|---|
task | string | all | review | compose | blocks | ship |
message | string | all | The work object: message text, or a Block Kit payload as JSON text. Clipped at 24 000 characters, head and tail kept, with a marker. |
kind | string | all | message or payload. The browser decides this; send what you have. |
channel | string | all | team | company | incident | customer | dm |
purpose | string | all | announce | request | update | decide | escalate |
tone | string | all | plain | warm | formal | urgent |
prescan | string | all | The client-side facts block. Optional over HTTP, but the model reconciles against it, so omitting it costs you the ## CHECKS section's value. |
interactivity | string | blocks, ship | none | ack | approve | select |
runtime | string | ship | both | python | js |
notes | string | review, compose | Anything the model should know about the audience. Optional. |
clipped | string | all | Present only when the work object was too large to send whole. For message text the middle is dropped and both ends are kept; for a Block Kit payload whole blocks are dropped from the middle so what arrives is still valid JSON. When present, the model says so in ## SUMMARY and in ## CHECKS, and its answer covers only what it saw. Omit it and the model assumes it received the entire input. |
A complete request body:
{
"task": "blocks",
"kind": "message",
"message": "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.",
"channel": "company",
"purpose": "announce",
"tone": "plain",
"interactivity": "ack",
"prescan": "CLIENT PRESCAN (computed in the browser, treat as ground truth):\n- message text: 63 characters over 1 line(s)\n- lint: 0 blocker(s), 0 warning(s), 0 nit(s)"
}1. Get a token
The shortest path is the token page: it shows the token this browser
already holds, with Copy token and Copy shell export buttons, and
a sign-in button for a personal token. A guest token can call /me and
/estimate; running a lane is metered and needs a personal token.
# The token page hands you a ready-made shell export:
# https://slack-desk.skillsafe.ai/tokens.html
# export SKILLSAFE_TOKEN="..."
#
# Or mint a guest token here. A guest can call /me and /estimate;
# running a lane needs a personal token from signing in.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" -H "X-App-Slug: slack-desk"
# {"ok":true,"data":{"token":"sk_guest_...","subject_type":"guest"}}
# Open https://slack-desk.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here.
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "slack-desk"
TOKEN = "YOUR_TOKEN" # paste it, or load it from your own secret store
def call(path, body=None, token=TOKEN):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data,
method="POST" if data else "GET")
req.add_header("X-App-Slug", SLUG)
req.add_header("Content-Type", "application/json")
if token:
req.add_header("Authorization", "Bearer " + token)
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"):
raise RuntimeError(out["error"]["code"] + ": " + out["error"]["message"])
return out["data"]
TOKEN = call("/guest", {}, token=None)["token"] # guest route
// Open https://slack-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "slack-desk";
let TOKEN = "YOUR_TOKEN"; // paste it, or load it from your own secret store
async function call(path, body, extraHeaders = {}) {
const headers = { "X-App-Slug": SLUG, "Content-Type": "application/json", ...extraHeaders };
if (TOKEN) headers.Authorization = `Bearer ${TOKEN}`;
const res = await fetch(BASE + path, {
method: body === undefined ? "GET" : "POST",
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const out = await res.json();
if (!out.ok) throw new Error(`${out.error.code}: ${out.error.message}`);
return out.data;
}
TOKEN = "";
TOKEN = (await call("/guest", {})).token;
// Open https://slack-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here.
const base = "https://api.skillsafe.ai/v1/app-api"
const slug = "slack-desk"
var token = "YOUR_TOKEN" // paste it, or load it from your own secret store
func call(path string, body any) (map[string]any, error) {
var rdr io.Reader
method := http.MethodGet
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("X-App-Slug", slug)
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var out struct {
OK bool `json:"ok"`
Data map[string]any `json:"data"`
Error map[string]any `json:"error"`
}
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
return nil, err
}
if !out.OK {
return nil, fmt.Errorf("%v: %v", out.Error["code"], out.Error["message"])
}
return out.Data, nil
}
// Open https://slack-desk.skillsafe.ai/tokens.html and press "Copy token".
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "slack-desk";
static String token = "YOUR_TOKEN"; // paste it, or load it from your own secret store
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("X-App-Slug", SLUG)
.header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
HttpRequest req = (jsonBody == null)
? b.GET().build()
: b.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body(); // { "ok": true, "data": { ... } }
}
# Open https://slack-desk.skillsafe.ai/tokens.html and press "Copy token".
require "json"
require "net/http"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
SLUG = "slack-desk"
TOKEN = "YOUR_TOKEN" # paste it, or load it from your own secret store
def call(path, body = nil)
uri = URI(BASE.to_s + path)
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["X-App-Slug"] = SLUG
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}" unless TOKEN.empty?
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
out = JSON.parse(res.body)
raise "#{out["error"]["code"]}: #{out["error"]["message"]}" unless out["ok"]
out["data"]
end
<?php
// Open https://slack-desk.skillsafe.ai/tokens.html and press "Copy token".
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "slack-desk";
$TOKEN = "YOUR_TOKEN"; // paste it, or load it from your own secret store
function call_api(string $path, ?array $body = null): array {
global $TOKEN;
$headers = ["X-App-Slug: " . SLUG, "Content-Type: application/json"];
if ($TOKEN !== "") { $headers[] = "Authorization: Bearer " . $TOKEN; }
$ch = curl_init(BASE . $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));
}
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!$out["ok"]) { throw new Exception($out["error"]["code"] . ": " . $out["error"]["message"]); }
return $out["data"];
}
// Open https://slack-desk.skillsafe.ai/tokens.html and press "Copy token".
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "slack-desk";
string token = "YOUR_TOKEN"; // paste it, or load it from your own secret store
var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-App-Slug", Slug);
if (token.Length > 0)
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
async Task<JsonElement> Call(string path, object? body = null) {
var res = body is null
? await http.GetAsync(Base + path)
: await http.PostAsJsonAsync(Base + path, body);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!doc.GetProperty("ok").GetBoolean())
throw new Exception(doc.GetProperty("error").GetProperty("code").GetString());
return doc.GetProperty("data");
}
2. Check the balance
/me is free and tells you who the token belongs to and what it can spend. Compare
credits against the hold_credits from the next step before you run, so a
402 never surprises you after submit.
curl -sS "https://api.skillsafe.ai/v1/app-api/me" \
-H "X-App-Slug: slack-desk" -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# {"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":48210}}
me = call("/me")
print(me["subject_type"], me["credits"])
const me = await call("/me");
console.log(me.subject_type, me.credits);
me, err := call("/me", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(me["subject_type"], me["credits"])
System.out.println(call("/me", null));
me = call("/me")
puts "#{me["subject_type"]} #{me["credits"]}"
$me = call_api("/me");
echo $me["subject_type"], " ", $me["credits"], "\n";
var me = await Call("/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
3. Price the run for free
/estimate takes the same body as /run, costs nothing and starts no job.
Estimate the lane you are about to run: the four lanes have different prompts and
different output caps, so hold_credits differs per lane. hold_credits is
what gets reserved, not what you pay; the settled charge is usually far lower.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "X-App-Slug: slack-desk" -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "blocks", "kind": "message", "message": "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.", "channel": "company", "purpose": "announce", "tone": "plain", "interactivity": "ack", "prescan": "CLIENT PRESCAN (computed in the browser, treat as ground truth):\n- message text: 63 characters over 1 line(s)\n- lint: 0 blocker(s), 0 warning(s), 0 nit(s)"}'
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":4180,"min_credits":320}}
INPUT = {
"task": "blocks",
"kind": "message",
"message": "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.",
"channel": "company",
"purpose": "announce",
"tone": "plain",
"interactivity": "ack",
"prescan": "CLIENT PRESCAN (computed in the browser, treat as ground truth):\n- message text: 63 characters over 1 line(s)\n- lint: 0 blocker(s), 0 warning(s), 0 nit(s)"
}
est = call("/estimate", INPUT)
assert est["model_alias"] == "gpt-terra"
if call("/me")["credits"] < est["hold_credits"]:
raise SystemExit("top up first: need %d" % est["hold_credits"])
const INPUT = {
"task": "blocks",
"kind": "message",
"message": "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.",
"channel": "company",
"purpose": "announce",
"tone": "plain",
"interactivity": "ack",
"prescan": "CLIENT PRESCAN (computed in the browser, treat as ground truth):\n- message text: 63 characters over 1 line(s)\n- lint: 0 blocker(s), 0 warning(s), 0 nit(s)"
};
const est = await call("/estimate", INPUT);
if ((await call("/me")).credits < est.hold_credits) {
throw new Error(`top up first: need ${est.hold_credits}`);
}
input := map[string]any{
"task": "blocks", "kind": "message",
"message": "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.",
"channel": "company", "purpose": "announce", "tone": "plain",
"interactivity": "ack",
}
est, err := call("/estimate", input)
if err != nil {
log.Fatal(err)
}
fmt.Println(est["hold_credits"], est["model_alias"])
String input = """
{
"task": "blocks",
"kind": "message",
"message": "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.",
"channel": "company",
"purpose": "announce",
"tone": "plain",
"interactivity": "ack",
"prescan": "CLIENT PRESCAN (computed in the browser, treat as ground truth):\n- message text: 63 characters over 1 line(s)\n- lint: 0 blocker(s), 0 warning(s), 0 nit(s)"
}
""";
System.out.println(call("/estimate", input));
INPUT = {
"task": "blocks",
"kind": "message",
"message": "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.",
"channel": "company",
"purpose": "announce",
"tone": "plain",
"interactivity": "ack",
"prescan": "CLIENT PRESCAN (computed in the browser, treat as ground truth):\n- message text: 63 characters over 1 line(s)\n- lint: 0 blocker(s), 0 warning(s), 0 nit(s)"
}
est = call("/estimate", INPUT)
abort("top up first") if call("/me")["credits"] < est["hold_credits"]
$input = [
"task" => "blocks",
"kind" => "message",
"message" => "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.",
"channel" => "company",
"purpose" => "announce",
"tone" => "plain",
"interactivity" => "ack",
"prescan" => "CLIENT PRESCAN (computed in the browser, treat as ground truth):\n- message text: 63 characters over 1 line(s)\n- lint: 0 blocker(s), 0 warning(s), 0 nit(s)"
];
$est = call_api("/estimate", $input);
if (call_api("/me")["credits"] < $est["hold_credits"]) {
throw new Exception("top up first");
}
var input = new {
task = "blocks", kind = "message",
message = "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.",
channel = "company", purpose = "announce", tone = "plain",
interactivity = "ack"
};
var est = await Call("/estimate", input);
Console.WriteLine(est.GetProperty("hold_credits"));
4. Run it and poll
/run returns a job_id; poll /jobs/{id} until it reaches a
terminal state. Always send an Idempotency-Key derived from the task, the input and
an attempt counter — a retry with the same key is not billed twice, and the same input under
two different tasks must use two different keys.
# One key per (task, input, attempt). Two lanes over one message are two runs.
KEY="sd-blocks-$(printf '%s' "$MESSAGE" | shasum | cut -c1-8)-1"
JOB=$(curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "X-App-Slug: slack-desk" -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" -H "Idempotency-Key: $KEY" \
-d '{"task": "blocks", "kind": "message", "message": "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.", "channel": "company", "purpose": "announce", "tone": "plain", "interactivity": "ack", "prescan": "CLIENT PRESCAN (computed in the browser, treat as ground truth):\n- message text: 63 characters over 1 line(s)\n- lint: 0 blocker(s), 0 warning(s), 0 nit(s)"}' | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')
until [ "$(curl -sS "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \
-H "X-App-Slug: slack-desk" -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["status"])')" = "succeeded" ]; do
sleep 2
done
import hashlib, time
key = "sd-%s-%s-1" % (INPUT["task"], hashlib.sha256(
json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:8])
job = call("/run", INPUT) # add the Idempotency-Key header in `call`
while True:
j = call("/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
text = j["output"]["text"]
const key = `sd-${INPUT.task}-${(await crypto.subtle.digest(
"SHA-256", new TextEncoder().encode(JSON.stringify(INPUT))
).then(b => [...new Uint8Array(b)].map(x => x.toString(16).padStart(2, "0")).join("").slice(0, 8)))}-1`;
const { job_id } = await call("/run", INPUT, { "Idempotency-Key": key });
let job;
do {
await new Promise(r => setTimeout(r, 2000));
job = await call(`/jobs/${job_id}`);
} while (!["succeeded", "failed", "cancelled"].includes(job.status));
const text = job.output.text;
job, err := call("/run", input) // set Idempotency-Key on the request
if err != nil {
log.Fatal(err)
}
id := job["job_id"].(string)
for {
j, _ := call("/jobs/"+id, nil)
if s := j["status"].(string); s == "succeeded" || s == "failed" {
break
}
time.Sleep(2 * time.Second)
}
String job = call("/run", input); // add Idempotency-Key to the builder
// poll /jobs/{id} every two seconds until status is terminal
require "digest"
key = "sd-#{INPUT["task"]}-#{Digest::SHA256.hexdigest(JSON.dump(INPUT))[0, 8]}-1"
job = call("/run", INPUT) # set req["Idempotency-Key"] = key inside call
loop do
j = call("/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
sleep 2
end
$key = "sd-" . $input["task"] . "-" . substr(hash("sha256", json_encode($input)), 0, 8) . "-1";
$job = call_api("/run", $input); // add the Idempotency-Key header inside call_api
do {
sleep(2);
$j = call_api("/jobs/" . $job["job_id"]);
} while (!in_array($j["status"], ["succeeded", "failed", "cancelled"], true));
var job = await Call("/run", input); // add Idempotency-Key to the request
string id = job.GetProperty("job_id").GetString()!;
JsonElement j;
do {
await Task.Delay(2000);
j = await Call($"/jobs/{id}");
} while (j.GetProperty("status").GetString() is not ("succeeded" or "failed" or "cancelled"));
5. Stream it instead
/run-stream is the same call over SSE. The deltas arrive as the model writes, so the
## section headings are what a progress indicator should key off. If the stream dies
mid-way, the text you already have still parses: sections are independent, and the app reports
“N of M sections recovered” rather than discarding the run.
curl -sS -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "X-App-Slug: slack-desk" -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" -H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d '{"task": "blocks", "kind": "message", "message": "Shipping v4.2 at 16:00 UTC. Billing moves to /account/billing.", "channel": "company", "purpose": "announce", "tone": "plain", "interactivity": "ack", "prescan": "CLIENT PRESCAN (computed in the browser, treat as ground truth):\n- message text: 63 characters over 1 line(s)\n- lint: 0 blocker(s), 0 warning(s), 0 nit(s)"}'
# data: {"type":"delta","text":"## LANE\nblocks\n\n## SUMMARY\n..."}
# data: {"type":"done","job_id":"job_..."}
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps(INPUT).encode(), method="POST")
for h, v in {"X-App-Slug": SLUG, "Content-Type": "application/json",
"Accept": "text/event-stream", "Idempotency-Key": key,
"Authorization": "Bearer " + TOKEN}.items():
req.add_header(h, v)
acc = ""
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if not line.startswith("data: "):
continue
ev = json.loads(line[6:])
if ev.get("type") == "delta":
acc += ev["text"]
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"X-App-Slug": SLUG, "Content-Type": "application/json",
"Accept": "text/event-stream", "Idempotency-Key": key,
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let acc = "", buf = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
for (const line of buf.split("\n")) {
if (!line.startsWith("data: ")) continue;
const ev = JSON.parse(line.slice(6));
if (ev.type === "delta") acc += ev.text;
}
buf = buf.slice(buf.lastIndexOf("\n") + 1);
}
req, _ := http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(b))
req.Header.Set("Accept", "text/event-stream")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
// json.Unmarshal([]byte(line[6:]), &ev); accumulate ev.Text
}
// HttpResponse.BodyHandlers.ofLines() over /run-stream, filtering "data: " lines
// and concatenating the `text` of every {"type":"delta"} event.
Net::HTTP.start(BASE.hostname, BASE.port, use_ssl: true) do |http|
req = Net::HTTP::Post.new(URI(BASE.to_s + "/run-stream"))
req["Accept"] = "text/event-stream"
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
req["Content-Type"] = "application/json"
req.body = JSON.dump(INPUT)
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
next unless line.start_with?("data: ")
ev = JSON.parse(line[6..])
print ev["text"] if ev["type"] == "delta"
end
end
end
end
$ch = curl_init(BASE . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($input));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-App-Slug: " . SLUG, "Content-Type: application/json",
"Accept: text/event-stream", "Authorization: Bearer " . $TOKEN,
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "data: ")) {
$ev = json_decode(substr($line, 6), true);
if (($ev["type"] ?? "") === "delta") { echo $ev["text"]; }
}
}
return strlen($chunk);
});
curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream") {
Content = JsonContent.Create(input)
};
req.Headers.Add("Accept", "text/event-stream");
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
while (await reader.ReadLineAsync() is string line) {
if (!line.StartsWith("data: ")) continue;
var ev = JsonDocument.Parse(line[6..]).RootElement;
if (ev.GetProperty("type").GetString() == "delta")
Console.Write(ev.GetProperty("text").GetString());
}
The output contract
Every lane returns plain text with ## section headings — one envelope, four
bodies. The app splits on ^##[ \t]+([A-Z][A-Z_]*)[ \t]*$, so a heading must be
uppercase, on its own line, with nothing after it. Sections are independent, which is what makes a
truncated stream still useful.
| section | lanes | form |
|---|---|---|
LANE | all | the lane id, lowercase, on one line |
SUMMARY | all | one to three sentences of prose |
VERDICT | review | post-it | fix-first | rewrite, then one sentence |
FINDINGS | review | bullets: [blocker|warn|nit] title :: what the reader sees :: the fix |
REWRITE | review | a ```text fence |
MESSAGE, SHORT, THREAD | compose | ```text fences; THREAD may be None. |
PAYLOAD | blocks, ship | a ```json fence: one object with blocks and text |
FALLBACK | blocks | a ```text fence matching the payload's text |
PYTHON, JAVASCRIPT, WEBHOOK, HANDLER | ship | fences tagged python, javascript, bash, python |
WHY | compose, blocks | three to six bullets |
CHECKS | all | bullets, one per prescan finding, naming the rule id |
A worked example per lane
Same message in every case — “Shipping v4.2 at 16:00 UTC. Billing moves to
/account/billing.” — with only task changed.
task: review
## LANE
review
## SUMMARY
The message says the right thing but posts three Markdown constructs that Slack renders literally.
## VERDICT
fix-first
The content is correct; the formatting is what will embarrass you in a company channel.
## FINDINGS
- [blocker] Markdown bold :: readers see the asterisks, nothing is bold :: use *v4.2*
- [warn] No owner named :: nobody knows who to ask at 16:00 :: add the on-call handle
## REWRITE
```text
Shipping *v4.2* at 16:00 UTC. Billing moves to /account/billing.
```
## CHECKS
- MD-BOLD line 1: fixed - **v4.2** became *v4.2*.task: compose
## LANE
compose
## SUMMARY
Led with the change and the time, and moved the bookmark detail into the thread.
## MESSAGE
```text
*v4.2 ships at 16:00 UTC today.* Billing moves to /account/billing.
```
## SHORT
```text
v4.2 at 16:00 UTC - billing moves to /account/billing.
```
## THREAD
```text
Old bookmarks keep redirecting for 30 days.
```
## WHY
- Put the time in the first clause because the purpose is an announcement in a company channel.
## CHECKS
- Prescan reported no findings; nothing to reconcile.task: blocks
## LANE
blocks
## SUMMARY
A header, one section and an acknowledge button, with a one-line notification fallback.
## PAYLOAD
```json
{
"text": "v4.2 ships at 16:00 UTC; billing moves to /account/billing.",
"blocks": [
{ "type": "header", "text": { "type": "plain_text", "text": "v4.2 ships at 16:00 UTC" } },
{ "type": "section", "text": { "type": "mrkdwn", "text": "Billing moves to `/account/billing`." } },
{ "type": "actions", "elements": [
{ "type": "button", "action_id": "ack_v42", "style": "primary",
"text": { "type": "plain_text", "text": "Acknowledged" } }
] }
]
}
```
## FALLBACK
```text
v4.2 ships at 16:00 UTC; billing moves to /account/billing.
```
## WHY
- The header carries the subject so the message is scannable in a busy channel.
## CHECKS
- Prescan reported no findings; nothing to reconcile.task: ship
## LANE
ship
## SUMMARY
Bolt for Python and for JavaScript post the same payload; one handler acknowledges the button.
## PAYLOAD
```json
{ "text": "v4.2 ships at 16:00 UTC", "blocks": [ ... as above ... ] }
```
## PYTHON
```python
from slack_bolt import App
app = App(token="SLACK_BOT_TOKEN_PLACEHOLDER") # load this from your secret manager
app.client.chat_postMessage(channel="#launch", text=TEXT, blocks=BLOCKS)
```
## JAVASCRIPT
```javascript
const { App } = require("@slack/bolt");
const app = new App({ token: "SLACK_BOT_TOKEN_PLACEHOLDER" });
await app.client.chat.postMessage({ channel: "#launch", text: TEXT, blocks: BLOCKS });
```
## WEBHOOK
```bash
curl -X POST -H 'Content-Type: application/json' \
--data @payload.json \
https://hooks.slack.com/services/T000/B000/XXXX
```
## HANDLER
```python
@app.action("ack_v42")
def ack_v42(ack, body, respond):
ack()
respond(text="Acknowledged by <@" + body["user"]["id"] + ">")
```
## CHECKS
- Prescan reported no findings; nothing to reconcile.Validate the payload before you post it
The app never displays a payload it could not parse and validate, and neither should your pipeline.
The cheapest gate is: parse the PAYLOAD fence with a real JSON parser, then assert
blocks.length <= 50, every header is plain_text and at most
150 characters, every section text is at most 3000, every button label is at most 75,
and no two elements share an action_id. A payload that fails any of those is rejected
by Slack at post time, which is the worst place to find out.