Drive the resume agent from your own code
Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS with optional streaming. This tutorial covers every agent task the app exposes — tailor, cover letter, refine, gap questions and gap apply — 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:
{"data": …} on success, {"error": {"code", "message"}} on failure.
Runs execute the app's agent (model gpt-5.6-terra) and are billed in SkillSafe
credits to the calling token, with a worst-case hold up front and the actual cost settled
when the job finishes.
| Status | Meaning |
|---|---|
401 | Missing or expired token — create a new session. |
402 | Not enough credits — top up at skillsafe.ai/account/credits. |
403 | The token isn't allowed to do this. |
404 | Unknown job or record id. |
5xx | Transient platform error — retry with backoff. |
Browsers enforce CORS for this API, so run these examples from a server, script or
terminal — not from another website's frontend. The app's own runs history
collection is an internal chunked store, not a public API surface — read your results from
the job output instead.
Step 0 — A tiny client
Every task below is one or two HTTP calls, so start with a small helper that adds the auth
header, sends JSON and unwraps the data envelope. The later steps reuse this
helper.
export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN" # see step 1
# every call looks like:
# curl -s "$API/…" -H "Authorization: Bearer $TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": …} envelope
import json, requests
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # see step 1
def api(method, path, body=None, **headers):
res = requests.request(method, API + path, json=body,
headers={"Authorization": f"Bearer {TOKEN}", **headers})
payload = res.json()
if not res.ok:
raise RuntimeError(payload.get("error", {}).get("message", res.reason))
return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 1 — read it from your environment in real code
async function api(method, path, body, extraHeaders = {}) {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const API = "https://api.skillsafe.ai/v1/app-api"
var token = os.Getenv("SKILLSAFE_TOKEN") // see step 1
func call(method, path string, body, out any) error {
var buf bytes.Buffer
if body != nil {
json.NewEncoder(&buf).Encode(body)
}
req, _ := http.NewRequest(method, API+path, &buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct {
Data json.RawMessage `json:"data"`
Error *struct{ Message string `json:"message"` } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if res.StatusCode >= 400 {
return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
}
if out == nil {
return nil
}
return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class SkillSafe {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see step 1
static final HttpClient HTTP = HttpClient.newHttpClient();
static String api(String method, String path, String jsonBody) throws Exception {
var req = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) throw new RuntimeException(res.body());
return res.body(); // envelope: {"data": …}
}
}
require "net/http"
require "json"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN") # see step 1
def api(method, path, body = nil)
uri = URI(API + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = body.to_json if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise (payload.dig("error", "message") || res.message) unless res.is_a?(Net::HTTPSuccess)
payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN"); // see step 1
function api(string $method, string $path, ?array $body = null): mixed {
global $TOKEN;
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$payload = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($payload["error"]["message"] ?? "HTTP $status");
}
return $payload["data"];
}
// .NET 8+
using System.Net.Http.Json;
using System.Text.Json;
static class SkillSafe
{
const string Api = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new();
static SkillSafe() =>
Http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see step 1
public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null)
{
var req = new HttpRequestMessage(method, Api + path);
if (body != null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!res.IsSuccessStatusCode)
throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
return json.GetProperty("data");
}
}
Step 1 — Get a token
For scripted use, the simplest reliable path is your personal token: open the
token page, sign in, and hit
"Copy shell export" — it puts export SKILLSAFE_TOKEN="…" on your clipboard,
which every example below reads. Treat the token like a password — it can spend your
credits. For fully headless scripts, POST /guest (below) mints a guest token
with no browser involved; guests can always call /me and
/estimate, but whether a guest can afford an actual run depends on the app's
daily sponsorship budget, so don't build on it.
curl -s -X POST "$API/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"tailored-resume"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "tailored-resume"})["token"]
const { token } = await api("POST", "/guest", { slug: "tailored-resume" });
var guest struct{ Token string `json:"token"` }
err := call("POST", "/guest", map[string]string{"slug": "tailored-resume"}, &guest)
String envelope = api("POST", "/guest", """
{"slug":"tailored-resume"}""");
// token is at data.token in the returned JSON
token = api("POST", "/guest", { slug: "tailored-resume" })["token"]
$token = api("POST", "/guest", ["slug" => "tailored-resume"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
new { slug = "tailored-resume" });
var token = guest.GetProperty("token").GetString();
Step 2 — Check who you are and your balance
Returns subject_type ("user" or "guest"),
subject_id and your credits balance. Check this before an
expensive run.
curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
SubjectType string `json:"subject_type"`
Credits int64 `json:"credits"`
}
err := call("GET", "/me", nil, &me)
String envelope = api("GET", "/me", null);
// data.subject_type, data.credits
me = api("GET", "/me")
puts "#{me["subject_type"]}: #{me["credits"]} credits"
$me = api("GET", "/me");
echo "{$me['subject_type']}: {$me['credits']} credits\n";
var me = await SkillSafe.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")}: {me.GetProperty("credits")} credits");
Step 3 — Estimate the cost
Send the same input you would send to a run; the response's hold_credits is
the worst-case cost and min_credits the floor. Nothing is charged and no job
is created. The response also reports the resolved model and whether
sponsorship is active.
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"job_description":"…","background":"…"}' | jq '.data'
est = api("POST", "/estimate", {"job_description": jd, "background": background})
print("worst case:", est["hold_credits"], "credits on", est["model"])
const est = await api("POST", "/estimate", { job_description: jd, background });
console.log("worst case:", est.hold_credits, "credits on", est.model);
var est struct {
HoldCredits int64 `json:"hold_credits"`
Model string `json:"model"`
}
err := call("POST", "/estimate", map[string]string{
"job_description": jd, "background": background,
}, &est)
String envelope = api("POST", "/estimate", """
{"job_description": %s, "background": %s}
""".formatted(toJsonString(jd), toJsonString(background)));
// worst-case cost is at data.hold_credits
est = api("POST", "/estimate", { job_description: jd, background: background })
puts "worst case: #{est["hold_credits"]} credits on #{est["model"]}"
$est = api("POST", "/estimate", [
"job_description" => $jd,
"background" => $background,
]);
echo "worst case: {$est['hold_credits']} credits on {$est['model']}\n";
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", new {
job_description = jd, background });
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits");
Step 4 — Task tailor: run it and wait
The main task. /run places a credit hold and returns a job_id;
poll /jobs/{job_id} every 1–2 seconds until status is
succeeded or failed. Always send an Idempotency-Key
header so a network retry can't start a second, double-charged run. The agent replies with
one JSON object, delivered as a string at output.output — parse it.
| Input field | Type | Notes |
|---|---|---|
task | string, optional | Omit or "tailor" for this task. |
job_description | string, required | The full job posting. |
background | string, required | Current resume, or free-form notes on roles, dates, skills, education. |
job_title_company | string, optional | e.g. "Senior Platform Engineer — Vercel". |
notes | string, optional | Extra instructions: career transition, page limit, sections to emphasize. |
retry_note | string, optional | Feedback about a previous malformed reply; the agent obeys it exactly. |
$model | string, optional | Per-run model override (allowlisted models only). |
# input.json: {"job_description":"…","background":"…","job_title_company":"…"}
JOB_ID=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: tailor-$(date +%s)" \
-d @input.json | jq -r '.data.job_id')
while :; do
JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN")
STATUS=$(echo "$JOB" | jq -r '.data.status')
[ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
sleep 2
done
echo "$JOB" | jq -r '.data.output.output' | jq '{name: .resume.name, matched: .keywords.matched}'
import time
job_id = api("POST", "/run", {
"job_description": jd,
"background": background,
"job_title_company": "Senior Platform Engineer — Vercel",
}, **{"Idempotency-Key": "tailor-001"})["job_id"]
while True:
job = api("GET", f"/jobs/{job_id}")
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1.5)
if job["status"] == "failed":
raise RuntimeError(job.get("error", "run failed"))
raw = job["output"]
if isinstance(raw, dict) and "output" in raw:
raw = raw["output"]
result = json.loads(raw) if isinstance(raw, str) else raw
print(result["resume"]["name"], "—", len(result["trace"]), "traced claims")
const { job_id } = await api("POST", "/run", {
job_description: jd,
background,
job_title_company: "Senior Platform Engineer — Vercel",
}, { "Idempotency-Key": crypto.randomUUID() });
let job;
do {
await new Promise((r) => setTimeout(r, 1500));
job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");
if (job.status === "failed") throw new Error(job.error ?? "run failed");
const raw = job.output?.output ?? job.output;
const result = typeof raw === "string" ? JSON.parse(raw) : raw;
console.log(result.resume.name, result.keywords.matched);
var started struct{ JobID string `json:"job_id"` }
err := call("POST", "/run", map[string]string{
"job_description": jd, "background": background,
}, &started)
if err != nil {
log.Fatal(err)
}
var job struct {
Status string `json:"status"`
Error string `json:"error"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
for {
if err := call("GET", "/jobs/"+started.JobID, nil, &job); err != nil {
log.Fatal(err)
}
if job.Status == "succeeded" || job.Status == "failed" {
break
}
time.Sleep(1500 * time.Millisecond)
}
var result map[string]any
json.Unmarshal([]byte(job.Output.Output), &result)
String envelope = api("POST", "/run", """
{"job_description": %s, "background": %s}
""".formatted(toJsonString(jd), toJsonString(background)));
String jobId = /* data.job_id via your JSON library */;
while (true) {
String job = api("GET", "/jobs/" + jobId, null);
String status = /* data.status */;
if (status.equals("succeeded") || status.equals("failed")) break;
Thread.sleep(1500);
}
// the tailored result is the JSON *string* at data.output.output —
// parse it again with your JSON library
started = api("POST", "/run", { job_description: jd, background: background })
job = nil
loop do
job = api("GET", "/jobs/#{started["job_id"]}")
break if %w[succeeded failed].include?(job["status"])
sleep 1.5
end
raise (job["error"] || "run failed") if job["status"] == "failed"
raw = job["output"].is_a?(Hash) ? job["output"].fetch("output", job["output"]) : job["output"]
result = raw.is_a?(String) ? JSON.parse(raw) : raw
puts "#{result["resume"]["name"]} — #{result["keywords"]["matched"].size} keywords matched"
$started = api("POST", "/run", [
"job_description" => $jd,
"background" => $background,
]);
do {
sleep(2);
$job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"]));
if ($job["status"] === "failed") {
throw new Exception($job["error"] ?? "run failed");
}
$raw = is_array($job["output"]) ? ($job["output"]["output"] ?? $job["output"]) : $job["output"];
$result = is_string($raw) ? json_decode($raw, true) : $raw;
echo $result["resume"]["name"] . "\n";
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", new {
job_description = jd, background });
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
var status = job.GetProperty("status").GetString();
if (status is "succeeded" or "failed") break;
await Task.Delay(1500);
}
var result = JsonDocument.Parse(
job.GetProperty("output").GetProperty("output").GetString()!).RootElement;
Console.WriteLine(result.GetProperty("resume").GetProperty("name"));
The tailoring result has this shape:
| Field | What it is |
|---|---|
resume | The tailored resume: name, headline, contact, summary, grouped skills, experience, education, plus projects / certifications / awards / extras when supported. |
trace | 4–8 load-bearing claims, each mapped to the employer and year in your history it came from. |
unmatched | Job requirements deliberately left out because your history can't substantiate them. |
keywords | {matched, missing} ATS terms. |
strengths, gaps, recommendations | Competitive strengths, gaps with mitigations, and concrete improvements. |
cover_letter, interview | Opening lines and interview talking points. |
If the reply ever fails to parse as JSON, retry once with the same input plus a
retry_note field describing the problem — the agent is instructed to obey it.
That's exactly what the app itself does.
Step 5 — The same run, streamed
Identical input to /run, but the response is
text/event-stream, so you can show output as it generates. Events:
| Event | Data |
|---|---|
job | {job_id} — the run was accepted. |
delta | {text} — the next chunk of agent output. |
done / pending | Final payload: {job_id, status, charged_credits, output}. Authoritative — deltas can drop the tail, so always read the result from here. |
error | {code, message, job_id}. |
curl -sN -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @input.json
# event: job data: {"job_id":"job_…"}
# event: delta data: {"text":"{\"resume\":{\"name\""}
# …
# event: done data: {"job_id":"…","status":"succeeded","charged_credits":412,
# "output":{"output":"…the full JSON result…"}}
res = requests.post(API + "/run-stream", json=payload, stream=True,
headers={"Authorization": f"Bearer {TOKEN}"})
event, done = None, None
for line in res.iter_lines(decode_unicode=True):
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data = json.loads(line[5:])
if event == "delta":
print(data.get("text", ""), end="", flush=True)
elif event in ("done", "pending"):
done = data
elif event == "error":
raise RuntimeError(data.get("message"))
result = json.loads(done["output"]["output"])
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", event = "message", done;
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buf += decoder.decode(chunk.value, { stream: true });
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i); buf = buf.slice(i + 1);
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) {
const data = JSON.parse(line.slice(5));
if (event === "delta") out += data.text ?? "";
else if (event === "done" || event === "pending") done = data;
else if (event === "error") throw new Error(data.message);
}
}
}
const result = JSON.parse(done.output.output);
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 1<<20), 1<<20)
event, done := "", []byte(nil)
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "event:") {
event = strings.TrimSpace(line[6:])
} else if strings.HasPrefix(line, "data:") {
data := strings.TrimSpace(line[5:])
if event == "delta" {
// unmarshal {"text": …} and append
} else if event == "done" || event == "pending" {
done = []byte(data)
}
}
}
// unmarshal done → .output.output (a JSON string) → your result struct
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payloadJson))
.build();
var lines = HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body();
final String[] event = {""};
StringBuilder doneData = new StringBuilder();
lines.forEach(line -> {
if (line.startsWith("event:")) event[0] = line.substring(6).trim();
else if (line.startsWith("data:")) {
if (event[0].equals("delta")) { /* parse {"text"} and append */ }
else if (event[0].equals("done")) doneData.append(line.substring(5).trim());
}
});
// parse doneData → output.output (a JSON string) → the result
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = payload.to_json
event, done, buf = nil, nil, ""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
buf << chunk
while (i = buf.index("\n"))
line = buf.slice!(0..i).chomp
if line.start_with?("event:") then event = line[6..].strip
elsif line.start_with?("data:")
data = JSON.parse(line[5..])
print data["text"] if event == "delta"
done = data if %w[done pending].include?(event)
end
end
end
end
end
result = JSON.parse(done["output"]["output"])
$event = ""; $done = null; $buf = "";
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $TOKEN", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$done, &$buf) {
$buf .= $chunk;
while (($i = strpos($buf, "\n")) !== false) {
$line = rtrim(substr($buf, 0, $i)); $buf = substr($buf, $i + 1);
if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
elseif (str_starts_with($line, "data:")) {
$data = json_decode(substr($line, 5), true);
if ($event === "delta") echo $data["text"] ?? "";
if ($event === "done" || $event === "pending") $done = $data;
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$result = json_decode($done["output"]["output"], true);
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream")
{ Content = JsonContent.Create(payload) };
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string? line; string ev = ""; JsonElement doneEl = default;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:"))
{
var data = JsonDocument.Parse(line[5..]).RootElement.Clone();
if (ev == "delta") Console.Write(
data.TryGetProperty("text", out var t) ? t.GetString() : "");
else if (ev is "done" or "pending") doneEl = data;
}
}
var result = JsonDocument.Parse(
doneEl.GetProperty("output").GetProperty("output").GetString()!).RootElement;
Step 6 — The four follow-up tasks
Everything after the first tailoring goes through the same two endpoints
(/run or /run-stream) — only the JSON body changes. Set
task to pick the job, and pass the resume object your
tailor run returned (including any edits you made) plus the original
job_description. Add today (e.g. "August 2, 2026")
for anything that needs a date. Each reply is again one JSON object as a string at
output.output.
Task cover_letter — a complete, sendable letter
{
"task": "cover_letter",
"job_description": "…", "resume": { …from the tailor result… },
"job_title_company": "Senior Platform Engineer — Vercel",
"hiring_manager": "Dana Wolf", // optional; never guessed
"tone": "direct", // "warm" | "direct" | "formal"
"today": "August 2, 2026"
}
// → {"letter": {"date", "recipient", "greeting", "paragraphs": […],
// "closing", "signature"}, "note": ""}
Task refine — rewrite a bullet, the summary, or the whole document
{
"task": "refine",
"scope": "bullet", // "bullet" | "summary" | "document"
"instruction": "lead with the metric",
"target": {"path": "experience.0.bullets.2", "text": "…current text…"},
"job_description": "…", "resume": { … }
}
// scope bullet/summary → {"options": [{"text", "why"} ×3], "note"}
// scope document → {"resume": {…complete rewrite…}, "trace": […],
// "keywords": {…}, "changes": […], "note"}
Task gap_questions — turn unmet requirements into questions
{
"task": "gap_questions",
"job_description": "…", "resume": { … },
"background": "…your original history text…",
"gaps": [ …from the tailor result… ],
"unmatched": [ …from the tailor result… ]
}
// → {"questions": [{"id", "requirement", "question", "why", "example"} ×3–6], "note"}
Task gap_apply — fold your answers back in, honestly
{
"task": "gap_apply",
"job_description": "…", "resume": { … },
"answers": [{"id": "q1", "requirement": "…", "question": "…",
"answer": "what you typed"}]
}
// → {"resume": {…updated…}, "trace": […], "added": [{"claim", "where", "from"}],
// "rejected": [{"id", "requirement", "why"}], "keywords": {…}, "note"}
// every answer lands in exactly one of added or rejected — vague answers are
// rejected rather than upgraded into experience
Calling any of them is the step-4 call with a different body:
# payload.json holds one of the four task bodies above
curl -s -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: task-$(date +%s)" \
-d @payload.json | jq -r '.data.job_id'
# …then poll /jobs/{job_id} exactly as in step 4
def run_task(payload, key):
job_id = api("POST", "/run", payload, **{"Idempotency-Key": key})["job_id"]
while True:
job = api("GET", f"/jobs/{job_id}")
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1.5)
if job["status"] == "failed":
raise RuntimeError(job.get("error", "run failed"))
return json.loads(job["output"]["output"])
letter = run_task({"task": "cover_letter", "job_description": jd,
"resume": result["resume"], "tone": "warm",
"today": "August 2, 2026"}, "letter-001")
print("\n\n".join(letter["letter"]["paragraphs"]))
async function runTask(payload) {
const { job_id } = await api("POST", "/run", payload,
{ "Idempotency-Key": crypto.randomUUID() });
let job;
do {
await new Promise((r) => setTimeout(r, 1500));
job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");
if (job.status === "failed") throw new Error(job.error ?? "run failed");
return JSON.parse(job.output.output);
}
const letter = await runTask({
task: "cover_letter", job_description: jd,
resume: result.resume, tone: "warm", today: "August 2, 2026",
});
// reuse the step-4 poll loop; only the body changes
payload := map[string]any{
"task": "cover_letter",
"job_description": jd,
"resume": result["resume"],
"tone": "warm",
"today": "August 2, 2026",
}
var started struct{ JobID string `json:"job_id"` }
if err := call("POST", "/run", payload, &started); err != nil {
log.Fatal(err)
}
// …poll /jobs/{job_id} as in step 4, then unmarshal output.output
// reuse the step-4 poll loop; only the body changes
String payload = """
{"task":"cover_letter","job_description": %s,
"resume": %s, "tone":"warm", "today":"August 2, 2026"}
""".formatted(toJsonString(jd), resumeJson);
String envelope = api("POST", "/run", payload);
// …poll /jobs/{job_id} as in step 4, then parse output.output
def run_task(payload)
started = api("POST", "/run", payload)
job = nil
loop do
job = api("GET", "/jobs/#{started["job_id"]}")
break if %w[succeeded failed].include?(job["status"])
sleep 1.5
end
raise (job["error"] || "run failed") if job["status"] == "failed"
JSON.parse(job["output"]["output"])
end
letter = run_task({ task: "cover_letter", job_description: jd,
resume: result["resume"], tone: "warm",
today: "August 2, 2026" })
function run_task(array $payload): array {
$started = api("POST", "/run", $payload);
do {
sleep(2);
$job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"]));
if ($job["status"] === "failed") {
throw new Exception($job["error"] ?? "run failed");
}
return json_decode($job["output"]["output"], true);
}
$letter = run_task([
"task" => "cover_letter", "job_description" => $jd,
"resume" => $result["resume"], "tone" => "warm",
"today" => "August 2, 2026",
]);
// reuse the step-4 poll loop; only the body changes
var payload = new {
task = "cover_letter", job_description = jd,
resume = resumeElement, tone = "warm", today = "August 2, 2026" };
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", payload);
// …poll /jobs/{job_id} as in step 4, then parse output.output
The honesty contract applies to every task: the agent never invents employers, dates,
degrees or metrics. Requirements your history can't support come back in
unmatched / rejected instead of being written into the resume —
that's the product, not a limitation.