← Flatpack Steps
Flatpack Steps API
Tokens

Drive Flatpack Steps from your own code

The one metered build is available over HTTP: give it what the sheet is for, who will follow it, a step limit and the notes as written, and it returns a verdict, a parts list lettered A, B, C, numbered steps that each do one thing (with the letters they use, a picture tag and an optional warning printed before the action), the questions the notes leave open, and each changed line with the rule it broke. A natural use is a house-manual tool or a holiday-rental welcome pack that wants instructions a guest can follow without a phone call.

The division of labour first: the page owns the check; the model builds the sheet; the page re-checks the reply. Splitting the notes into candidate steps, the rule tags (S1 one action, S2 verb first, S3 20 words, S4 no vague words, and "never says"), the number list and the verdict (by the same ordered rules the model gets) come from flatkit.js, which runs in node as well as in the browser. Its check function holds each reply to the notes: the step limit, 20 words per step, verb-first openers, part letters that exist, every number from the notes on the sheet and no number they never gave, questions only for gaps, changes that quote real note lines, no internal field names in prose.

Get a token on the token page (step 2 below also shows how to mint a guest token in code).

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"flatpack-steps"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field such as task or notes is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the build is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field

Every run sends task: sheet - there is one lane. The verdict is decided by four ordered, non-overlapping rules; the first that matches wins:

OrderVerdictWhen
1gapsA step cannot be done because the notes treat a needed fact as known and never give it ("the usual spot", "like last time", "the code" with no code). The sheet carries [ask: ...] placeholders and a question per missing fact.
2rebuildFewer than three separate steps, a step that needs something only done later, or more than half of the candidate steps breaking a sentence rule.
3tightenAt least one candidate step breaks a sentence rule (S1-S4); the rest keep their wording.
4readyEverything else: at most two small changes.

The declared input schema carries scalars only, so array-valued fields travel JSON-encoded as strings (FlatKit.toWire does this). Every value is a string.

Input fields

FieldTypeNotes
taskstringsheet.
jobstringWhat the sheet is for, up to 100 characters (Bin night).
for_whomstringOne of house-sitter, babysitter, guest, parent or grandparent, roommate, coworker, kid, future me, or empty.
steps_maxstring6, 10 or 14; anything else becomes 10.
notesstringThe notes as written. Past 4,000 characters whole lines are cut from the middle with a marker. After an ask-first sheet the page appends an Answers: block, one question: answer per line; the model treats each answer as part of the notes.
page_stepsstring (JSON)Array of strings, one per candidate step: "3. [S1 two actions] Fill the green can at the kitchen tap and then water the herbs on the balcony.".
page_numbersstring (JSON)Array of every number in the notes (["3"]).
page_leanstringThe page's verdict from its word lists, by the rules above.
page_flagsstring (JSON)Array of the page's warnings (a thing never said, more steps than the limit).
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object: task, verdict, title, summary, parts ([{id, name, qty}], qty empty unless the notes give a count), steps ([{action, parts: [ids], icon, warning}], icon one of do, look, wait, warn, two, lock, water, heat, power, call, bin, door), questions ([{ask, step}], only for gaps) and changes ([{was, now, why}], 0-2 for ready). Parse it with FlatKit.parseReply, then normalize(reply), then check(reply, form), which returns {notes}.

Worked examples

Replies produced against the app's prompt for the five samples (made-up homes with made-up notes; long fields and lists shortened; the full replies ship in example-result.js). Together they cover every verdict.

plants for a house-sitter - tighten

Input:

{
 "task": "sheet",
 "job": "Water the plants while we're away",
 "for_whom": "house-sitter",
 "steps_max": "10",
 "notes": "Water the big fern in the living room every 3 days.\nGive the cactus on the windowsill a bit of water once a week.\nFill the green can at the  ... (271 characters)",
 "page_steps": "[\"1. Water the big fern in the living room every 3 days.\",\"2. [S4 vague: a bit] Give the cactus on the windowsill a bit of water once a week.\",\"3. [S1 two actions] Fill the green c ... (333 characters)",
 "page_numbers": "[\"3\"]",
 "page_lean": "tighten",
 "page_flags": "[]"
}

Reply:

{
 "task": "sheet",
 "verdict": "tighten",
 "title": "Watering the Plants While Away",
 "summary": "This is a tighten: the cactus step used a vague amount and the can step held two actions, so those are the only two lines that needed fixing; the rest of your notes already read as clear steps. The cactus step now asks for water without guessing an amount, and filling the can is now its own step before watering the herbs. Everything else is copied from your notes as written.",
 "parts": [
  {
   "id": "A",
   "name": "the big fern in the living room",
   "qty": ""
  },
  {
   "id": "B",
   "name": "the cactus on the windowsill",
   "qty": ""
  },
  {
   "id": "C",
   "name": "the green can",
   "qty": ""
  },
  "... (6 in all)"
 ],
 "steps": [
  {
   "action": "Water the big fern in the living room every 3 days.",
   "parts": [
    "A"
   ],
   "icon": "water",
   "warning": ""
  },
  {
   "action": "Give the cactus on the windowsill water once a week.",
   "parts": [
    "B"
   ],
   "icon": "water",
   "warning": ""
  },
  {
   "action": "Fill the green can at the kitchen tap.",
   "parts": [
    "C"
   ],
   "icon": "water",
   "warning": ""
  },
  "... (6 in all)"
 ],
 "questions": [],
 "changes": [
  {
   "was": "Give the cactus on the windowsill a bit of water once a week.",
   "now": "Give the cactus on the windowsill water once a week.",
   "why": "Vague amount \"a bit\" with no amount given in the notes - removed (S4)."
  },
  {
   "was": "Fill the green can at the kitchen tap and then water the herbs on the balcony.",
   "now": "Fill the green can at the kitchen tap. / Water the herbs on the balcony.",
   "why": "Two actions in one step - split into two steps (S1)."
  }
 ]
}

the new TV for a parent, one paragraph - rebuild

Input:

{
 "task": "sheet",
 "job": "Watch the cooking channel on the new TV",
 "for_whom": "parent or grandparent",
 "steps_max": "6",
 "notes": "You need to use the black remote, not the silver one, the silver one is for the soundbar. Press the round button at the top and wait a bit f ... (331 characters)",
 "page_steps": "[\"1. [S2 hedged opener] You need to use the black remote, not the silver one, the silver one is for the soundbar.\",\"2. [S1 two actions, S4 vague: a bit] Press the round button at t ... (442 characters)",
 "page_numbers": "[]",
 "page_lean": "rebuild",
 "page_flags": "[]"
}

Reply:

{
 "task": "sheet",
 "verdict": "rebuild",
 "title": "Watch the Cooking Channel",
 "summary": "This is a rebuild: three of the four candidate steps broke a sentence rule (a hedged opener, two actions bundled into one sentence, a vague time word, and an overlong sentence), so the sheet was written fresh from your notes' facts. Pressing the button and waiting for the screen are now separate steps, and so are pressing Home, going to the app, and finding the channel. The remote mix-up became a warning on step 1 instead of its own step, which kept the sheet at 6 steps without dropping any fact.",
 "parts": [
  {
   "id": "A",
   "name": "black remote",
   "qty": ""
  },
  {
   "id": "B",
   "name": "soundbar",
   "qty": ""
  }
 ],
 "steps": [
  {
   "action": "Press the round button at the top.",
   "parts": [
    "A"
   ],
   "icon": "power",
   "warning": "Do not use the silver remote - it is for the soundbar."
  },
  {
   "action": "Wait for the screen to come on.",
   "parts": [],
   "icon": "wait",
   "warning": ""
  },
  {
   "action": "Press Home on the black remote.",
   "parts": [
    "A"
   ],
   "icon": "do",
   "warning": ""
  },
  "... (6 in all)"
 ],
 "questions": [],
 "changes": [
  {
   "was": "You need to use the black remote, not the silver one, the silver one is for the soundbar.",
   "now": "Do not use the silver remote - it is for the soundbar.",
   "why": "Hedged opener (S2) - turned into step 1's warning instead of a separate step."
  },
  {
   "was": "Press the round button at the top and wait a bit for the screen to come on.",
   "now": "Press the round button at the top. / Wait for the screen to come on.",
   "why": "Two actions joined with 'and' (S1) and a vague time word (S4) - split into two steps and dropped the vague word since no amount was given."
  },
  "... (4 in all)"
 ]
}

keys and heating for guests - gaps (ask first)

Input:

{
 "task": "sheet",
 "job": "Heating and doors for our guests",
 "for_whom": "guest",
 "steps_max": "10",
 "notes": "Front door key is in the usual spot.\nTurn the heating on at the panel in the hall.\nSet it to 20.\nPut in the code to turn the alarm off when  ... (181 characters)",
 "page_steps": "[\"1. [G never says: \\\"the usual spot\\\"] Front door key is in the usual spot.\",\"2. Turn the heating on at the panel in the hall.\",\"3. Set it to 20.\",\"4. [G never says: \\\"the code\\\"] ... (272 characters)",
 "page_numbers": "[\"20\"]",
 "page_lean": "gaps",
 "page_flags": "[\"Step 1 leans on \\\"the usual spot\\\" but your notes never say it.\",\"Step 4 leans on \\\"the code\\\" but your notes never say it.\"]"
}

Reply:

{
 "task": "sheet",
 "verdict": "gaps",
 "title": "Heating and Doors for Guests",
 "summary": "This is a gaps sheet: the notes treat the front door key's spot and the alarm code as already known but never state them, so two steps are missing a fact the reader cannot work out alone. I built the full five-step sequence anyway, adding a placeholder and a question for each missing fact, moved the alarm step's \"when you come in\" condition to the front of its sentence, and named the heating instead of the pronoun \"it\" in the temperature step. The heating-on and back-door steps are otherwise copied from the notes as written.",
 "parts": [
  {
   "id": "A",
   "name": "the front door key",
   "qty": ""
  },
  {
   "id": "B",
   "name": "the panel in the hall",
   "qty": ""
  },
  {
   "id": "C",
   "name": "the alarm",
   "qty": ""
  },
  "... (4 in all)"
 ],
 "steps": [
  {
   "action": "Get the front door key [ask: where the key is].",
   "parts": [
    "A"
   ],
   "icon": "lock",
   "warning": ""
  },
  {
   "action": "Turn the heating on at the panel in the hall.",
   "parts": [
    "B"
   ],
   "icon": "heat",
   "warning": ""
  },
  {
   "action": "Set the heating to 20.",
   "parts": [
    "B"
   ],
   "icon": "heat",
   "warning": ""
  },
  "... (5 in all)"
 ],
 "questions": [
  {
   "ask": "Where is the front door key kept?",
   "step": 1
  },
  {
   "ask": "What is the alarm code?",
   "step": 4
  }
 ],
 "changes": [
  {
   "was": "Front door key is in the usual spot.",
   "now": "Get the front door key [ask: where the key is].",
   "why": "The notes never say where the usual spot is - added a placeholder and a question (gap)."
  },
  {
   "was": "Set it to 20.",
   "now": "Set the heating to 20.",
   "why": "Named the heating instead of the pronoun \"it\" so one thing keeps one name (S5)."
  },
  "... (3 in all)"
 ]
}

bin night for a roommate - ready

Input:

{
 "task": "sheet",
 "job": "Bin night",
 "for_whom": "roommate",
 "steps_max": "6",
 "notes": "1. Take the green bin to the curb on Sunday night.\n2. Put the glass box next to the green bin.\n3. Bring both back in on Monday after 10am.\n4 ... (176 characters)",
 "page_steps": "[\"1. Take the green bin to the curb on Sunday night.\",\"2. Put the glass box next to the green bin.\",\"3. Bring both back in on Monday after 10am.\",\"4. Rinse the glass box with the h ... (186 characters)",
 "page_numbers": "[\"10\"]",
 "page_lean": "ready",
 "page_flags": "[]"
}

Reply:

{
 "task": "sheet",
 "verdict": "ready",
 "title": "Bin Night Steps",
 "summary": "This sheet is ready: the notes already give four clear, one-action steps in the right order with no missing facts. Nothing needed to change beyond listing the green bin, the glass box and the hose as parts; the page's own verdict of ready agrees.",
 "parts": [
  {
   "id": "A",
   "name": "the green bin",
   "qty": ""
  },
  {
   "id": "B",
   "name": "the glass box",
   "qty": ""
  },
  {
   "id": "C",
   "name": "the hose",
   "qty": ""
  }
 ],
 "steps": [
  {
   "action": "Take the green bin to the curb on Sunday night.",
   "parts": [
    "A"
   ],
   "icon": "bin",
   "warning": ""
  },
  {
   "action": "Put the glass box next to the green bin.",
   "parts": [
    "A",
    "B"
   ],
   "icon": "bin",
   "warning": ""
  },
  {
   "action": "Bring both back in on Monday after 10am.",
   "parts": [
    "A",
    "B"
   ],
   "icon": "bin",
   "warning": ""
  },
  "... (4 in all)"
 ],
 "questions": [],
 "changes": []
}

a router reset for future me, one run-on line - rebuild

Input:

{
 "task": "sheet",
 "job": "Reset the wifi router",
 "for_whom": "future me",
 "steps_max": "6",
 "notes": "Unplug the router, wait 30 seconds and plug it back in, then hold the reset pin for 10 seconds if the lights stay orange and wait for the gr ... (150 characters)",
 "page_steps": "[\"1. [S1 two actions, S3 long, 30 words] Unplug the router, wait 30 seconds and plug it back in, then hold the reset pin for 10 seconds if the lights stay orange and wait for the g ... (193 characters)",
 "page_numbers": "[\"30\",\"10\"]",
 "page_lean": "rebuild",
 "page_flags": "[\"Fewer than three steps - say each thing to do on its own line.\"]"
}

Reply:

{
 "task": "sheet",
 "verdict": "rebuild",
 "title": "Reset the wifi router",
 "summary": "This is a rebuild: the notes gave only one run-on step covering five actions, which is fewer than three separate steps and breaks the one-action rule. I split the notes into five single-action steps in their original order, keeping the 30-second wait, the 10-second reset-pin hold only if the lights stay orange, and the final wait for the green light.",
 "parts": [
  {
   "id": "A",
   "name": "the router",
   "qty": ""
  },
  {
   "id": "B",
   "name": "the reset pin",
   "qty": ""
  }
 ],
 "steps": [
  {
   "action": "Unplug the router.",
   "parts": [
    "A"
   ],
   "icon": "power",
   "warning": ""
  },
  {
   "action": "Wait 30 seconds.",
   "parts": [],
   "icon": "wait",
   "warning": ""
  },
  {
   "action": "Plug the router back in.",
   "parts": [
    "A"
   ],
   "icon": "power",
   "warning": ""
  },
  "... (5 in all)"
 ],
 "questions": [],
 "changes": [
  {
   "was": "Unplug the router, wait 30 seconds and plug it back in, then hold the reset pin for 10 seconds if the lights stay orange and wait for the green light.",
   "now": "Unplug the router. / Wait 30 seconds. / Plug the router back in. / If the lights stay orange, hold the reset pin for 10 seconds. / Wait for the green light.",
   "why": "One run-on sentence with five actions - split into five single-action steps (rebuild)."
  }
 ]
}

Build the input yourself

Load the script in node (global.window = global; require("./flatkit.js")), then:

const K = FlatKit;
const form = { job: "Bin night", for_whom: "roommate", steps_max: "6",
  notes: "1. Take the green bin to the curb on Sunday night.\n2. Put the glass box next to the green bin.\n..." };
console.log(K.lean(form), K.scan(form.notes));           // the page's verdict and its candidate steps with rule tags
const wire = K.toWire(K.buildInput(form).input);          // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// const { notes } = K.check(reply, form);                // the page's re-check of the reply
// K.plainText(reply)                                     // the sheet as plain text
// K.lint("You should water it a bit and then lock up.")  // S1, S2 and S4 tags

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://flatpack-steps.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"flatpack-steps"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "sheet", "job": "Bin night", "for_whom": "roommate", "steps_max": "6", "notes": "1. Take the green bin to the curb on Sunday night.\n...", "page_steps": "[...]", "page_numbers": "[\"10\"]", "page_lean": "ready", "page_flags": "[]"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses flatpack-steps:<task>:<inputHash>:a<attempt> (there is one lane, sheet), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="flatpack-steps:sheet:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: flatpack-steps:sheet:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json