Drive Late Pile from your own code
The one metered task, triage, is available over HTTP. It takes a pile of late tasks as the page reads it - what is going on, today's date, the minutes of task work each day can hold, every working day ahead as one numbered line (d1, d2 ...), every late task as one numbered line (t1, t2 ...) with its due date, how late it is, its minutes, an urgent mark and its notes, the totals, the page's plain plan and what it left out - and returns one action per task (do it on a given plan day with a first step, hand it to someone with a message, ask whether it is still needed with a message, or leave it for later), each with a reason, a verdict (clears or overloaded) and a note.
The division of labour first: the page reads the list and works out every due date, lateness and minute exactly, lays out the days ahead and builds the plain plan; the model decides what happens to each task and writes the first steps and messages, referring to tasks and days only by id so the page can check every one. The page reads up to 40 lines, plans up to 30 late tasks, counts a task with no estimate as 30 minutes, and leaves out tasks that are not due yet or have no date. After the reply it keeps one action per task it sent, refuses any day the reply would overfill, lists a task the reply left out under later, re-checks the verdict against what is left for later and every number the reply quotes, and builds the checklist and the CSV. Your code can do the same with the engine files the page loads (see "Build the input yourself").
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": "..." } }
The token is minted for this app (the guest call takes {"slug":"late-pile"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, context, today, capacity, plan_days, tasks, totals, page_plan, not_sent) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the triage run is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: triage (the one task is triage); a missing or different one is still answered as triage. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always triage. |
context | yes | What is going on, in the person's words (at most 160 characters): Back from a week of leave, freelance design work, or none. |
today | yes | Today's date as the page counts lateness from it: Wed 28 Oct 2026. |
capacity | yes | How many minutes of task work each plan day can hold, on the quarter hour: 180 minutes a day. |
plan_days | yes | One line per working day ahead (3, 5, 7 or 10 of them), (today) marking today: d1. Wed 28 Oct (today) - 180 min. |
tasks | yes | One line per late task, at most 30: t2. Export final logo files for Fennel Bakery | due Thu 22 Oct, 6 days late | 60 min | notes: client asked twice - id, title, due date and how late, minutes ((assumed) when no estimate was given), urgent when marked, and notes when there are any. |
totals | yes | The number of tasks, the minutes of work and the minutes of room: 9 tasks, 510 minutes of work; 900 minutes of room over 5 days. |
page_plan | yes | The page's plain plan, one line per day and then what it left for later: d1: t1, t6, t5 (180 of 180 min) ... later: none. |
not_sent | yes | The tasks the page left out because they are not due yet or have no date: 1 not due yet, or none. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
How the page reads the list (PileKit): one task per line, up to 40 lines. In the pipe form (title | due | time | ! | notes) the first part is the title and each other part is read as a due date, a time estimate, an urgent mark or notes; in free text the date and the estimate are found anywhere and the rest is the title. Due dates: 2026-10-20, 10/20 (or 20/10 when the first number is over 12), Oct 20, 20 Oct, today, yesterday, 3 days ago, 2 weeks ago, last week, Fri or last Fri; a date with no year is this year unless that lands more than 60 days after today, then last year. Estimates: 20m, 45 min, 1h, 1.5 hours, 1h30, from 5 minutes to 8 hours; a task with none counts as 30 minutes, marked (assumed). Urgent marks: !, urgent, asap, high, p1 or important. Lateness is counted in whole days against today (any date you set); a task due today is sent as due today, and tasks not due yet or with no date are counted in not_sent and left out. The plan days are the next working days from today in the chosen week (Mon-Fri, Sun-Thu, Mon-Sat or every day), each holding the same capacity. The page needs at least one task that is late or due today before it will send.
The plain plan (PileKit.freePlan) needs no run: it takes the urgent tasks first, then the latest, then the list order, and puts each on the first day with room; a task longer than a whole day gets an empty day to itself, and whatever finds no day is left for later. It is sent as page_plan and shown, as a checklist, before any run.
The model's rules: exactly one action for every task id, in id order. hand_off only when the task's own line names another person or team who could do it instead, with a message of at most 35 words to them naming the task and its due date; ask only when the task's notes say it may no longer be needed - being late is never a reason - with a message of at most 30 words asking whether it is still needed; do for every other task, on a plan day id, urgent tasks on the earliest days, then the latest, with a first step of at most 12 words taken from the task's own words; later for a task that cannot fit. The minutes on any one day must not exceed the capacity, except that a single task longer than the capacity may sit alone on a day of its own. Every action has a reason of at most 18 words using the task's due date, lateness, minutes, urgency or notes. The verdict is clears when no task is left for later, otherwise overloaded; the note says how many tasks land on which days and how many minutes today, and for an overloaded pile how many wait. Sentences name tasks by title and days by day and date, never by id; they quote only numbers from the input or sums of planned minutes, and never tell the person to drop, abandon, ignore or delete a task.
The reply
One JSON object:
{"task": "triage", "verdict": "overloaded", "actions": [{"id": "t1", "action": "do", "day": "d1", "first_step": "...", "message": "", "why": "..."}], "note": "..."}
| Field | What it holds |
|---|---|
verdict | clears (no task is left for later) or overloaded (the plan days cannot hold them all). |
actions[].id | The id of a task the page sent; one action per task, in id order. |
actions[].action | do, hand_off, ask or later. |
actions[].day | For do: the plan day id (d1 ...). Otherwise "". |
actions[].first_step | For do: the first physical action, at most 12 words, from the task's own words. Otherwise "". |
actions[].message | For hand_off and ask: one sentence to send - asking the named person to take the task on, or asking whether it is still needed. Otherwise "". |
actions[].why | One sentence: why this action, using the task's due date, lateness, minutes, urgency or notes. |
note | One sentence: how many tasks land on which days and the minutes today, and for overloaded, how many wait. |
The page reads the reply with ReplyKit.normalize (ids and actions matched in any case) and keeps what it trusts with ReplyKit.trusted(reply, built): an action is set aside, with the reason, when it names a task the page did not send, when its task already has an action, when its action is not one of the four, when a do has no plan day, or when a do would overfill its day (more minutes than the capacity, unless the task is longer than a whole day and sits alone). A task with no kept action is listed under later. A first step is kept only on do and a message only on hand_off and ask; titles, dates and minutes always come from the page's own reading of the list, never from the reply. The number of tasks left for later is what the page checks the verdict against.
ReplyKit.check(reply, built) lists, under "The page's check": a missing verdict, or one that what is left for later contradicts; every action set aside; every task the reply left out; a task left for later while a plan day still has room for it; a hand-off or a question with no message; a task to do with no first step; a hand-off whose line names no one; an urgent task placed after one that is not; any number in the note, a first step, a reason or a message that is not in the input or the plan's own sums; a sentence that names an id, an internal field or uses backticks; any word telling you to drop, abandon, ignore, delete or skip a task when your list never does; and words such as "again", "already", "always", "never" or "every" that your list and context never used. Any warning makes the status flagged; otherwise it is kept. The verdict on the page reads "The pile clears" or "More than the days can hold".
The downloads: ReplyKit.plain(form, t) copies the plan as a checklist, day by day, then the hand-offs, the questions and what waits; ReplyKit.csv(built, t) writes one row per task (id, task, due, days late, minutes, urgent, action, day, first step, message) with CRLF line ends; ReplyKit.md and ReplyKit.json write the plan with the messages, and the slip line picture saves as PNG or SVG.
Worked examples - every sample
These are the free samples on the page: the list as typed, the tasks and plan days the page reads, its plain plan, the exact input it sends, the reply that replays for free, and what the page does with it - the plan it trusts, the verdict against what is left for later and its own check. Together they cover the verdicts clears and overloaded and the actions do, hand_off, ask, later.
Back from a week off
The form: Back from a week of leave, freelance design work; today Wed 28 Oct 2026, 3 h of task work a day over the next 5 working days (Mon-Fri). The list as typed:
Send October invoice to Harbor Studio | Oct 23 | 20 min | !
Export final logo files for Fennel Bakery | Oct 22 | 1h | client asked twice
Reply to Ola about the brochure proofs | Oct 24 | 15 min
Update portfolio with the museum project | Oct 9 | 2h
Book accountant call | Oct 21 | 10 min
Revise homepage mockups for Kite Labs | Oct 27 | 2h 30m | !
Back up the work drive | Oct 20 | 30 min
Renew font licence | Oct 26 | 15 min | Sam at Kite Labs said they might buy it for the project
Write case study draft | Oct 16 | 1h30
Prepare workshop slides | Nov 5 | 2h
The late tasks as the page reads them, with how late each is (9 tasks, 510 minutes of work; 900 minutes of room over 5 days; left out: 1 not due yet), and the plan days (PileKit.buildInput):
t1. Send October invoice to Harbor Studio | due Fri 23 Oct, 5 days late | 20 min | urgent
t2. Export final logo files for Fennel Bakery | due Thu 22 Oct, 6 days late | 60 min | notes: client asked twice
t3. Reply to Ola about the brochure proofs | due Sat 24 Oct, 4 days late | 15 min
t4. Update portfolio with the museum project | due Fri 9 Oct, 19 days late | 120 min
t5. Book accountant call | due Wed 21 Oct, 7 days late | 10 min
t6. Revise homepage mockups for Kite Labs | due Tue 27 Oct, 1 day late | 150 min | urgent
t7. Back up the work drive | due Tue 20 Oct, 8 days late | 30 min
t8. Renew font licence | due Mon 26 Oct, 2 days late | 15 min | notes: Sam at Kite Labs said they might buy it for the project
t9. Write case study draft | due Fri 16 Oct, 12 days late | 90 min
d1. Wed 28 Oct (today) - 180 min
d2. Thu 29 Oct - 180 min
d3. Fri 30 Oct - 180 min
d4. Mon 2 Nov - 180 min
d5. Tue 3 Nov - 180 min
The page warns: "1 not due yet - left out of the plan."
The page's plain plan, made with no run (ReplyKit.plain(form, ReplyKit.fromPlan(built))):
Wed 28 Oct (today)
[ ] Send October invoice to Harbor Studio (20 min)
[ ] Book accountant call (10 min)
[ ] Revise homepage mockups for Kite Labs (150 min)
Thu 29 Oct
[ ] Reply to Ola about the brochure proofs (15 min)
[ ] Update portfolio with the museum project (120 min)
[ ] Back up the work drive (30 min)
[ ] Renew font licence (15 min)
Fri 30 Oct
[ ] Export final logo files for Fennel Bakery (60 min)
[ ] Write case study draft (90 min)
Input:
{
"task": "triage",
"context": "Back from a week of leave, freelance design work",
"today": "Wed 28 Oct 2026",
"capacity": "180 minutes a day",
"plan_days": "d1. Wed 28 Oct (today) - 180 min\nd2. Thu 29 Oct - 180 min\nd3. Fri 30 Oct - 180 min\nd4. Mon 2 Nov - 180 min\nd5. Tue 3 Nov - 180 min",
"tasks": "t1. Send October invoice to Harbor Studio | due Fri 23 Oct, 5 days late | 20 min | urgent\nt2. Export final logo files for Fennel Bakery | due Thu 22 Oct, 6 days late | 60 min | notes: client asked twice\nt3. Reply to Ola about the brochure proofs | due Sat 24 Oct, 4 days late | 15 min\nt4. Update portfolio with the museum project | due Fri 9 Oct, 19 days late | 120 min\nt5. Book accountant call | due Wed 21 Oct, 7 days late | 10 min\nt6. Revise homepage mockups for Kite Labs | due Tue 27 Oct, 1 day late | 150 min | urgent\nt7. Back up the work drive | due Tue 20 Oct, 8 days late | 30 min\nt8. Renew font licence | due Mon 26 Oct, 2 days late | 15 min | notes: Sam at Kite Labs said they might buy it for the project\nt9. Write case study draft | due Fri 16 Oct, 12 days late | 90 min",
"totals": "9 tasks, 510 minutes of work; 900 minutes of room over 5 days",
"page_plan": "d1: t1, t6, t5 (180 of 180 min)\nd2: t4, t7, t3, t8 (180 of 180 min)\nd3: t9, t2 (150 of 180 min)\nd4: nothing (0 of 180 min)\nd5: nothing (0 of 180 min)\nlater: none",
"not_sent": "1 not due yet"
}
Reply:
{
"task": "triage",
"verdict": "clears",
"actions": [
{
"id": "t1",
"action": "do",
"day": "d1",
"first_step": "Send the October invoice to Harbor Studio.",
"message": "",
"why": "Sending the Harbor Studio invoice is urgent and 5 days late; it takes 20 minutes."
},
{
"id": "t2",
"action": "do",
"day": "d3",
"first_step": "Export the final logo files for Fennel Bakery.",
"message": "",
"why": "The Fennel Bakery logo files are 6 days late at 60 minutes, and the client has asked twice."
},
{
"id": "t3",
"action": "do",
"day": "d2",
"first_step": "Reply to Ola about the brochure proofs.",
"message": "",
"why": "The reply to Ola about the brochure proofs is 4 days late and takes 15 minutes."
},
{
"id": "t4",
"action": "do",
"day": "d2",
"first_step": "Update the portfolio with the museum project.",
"message": "",
"why": "Updating the portfolio with the museum project is 19 days late and takes 120 minutes."
},
{
"id": "t5",
"action": "do",
"day": "d1",
"first_step": "Book the accountant call.",
"message": "",
"why": "Booking the accountant call is 7 days late and takes 10 minutes."
},
{
"id": "t6",
"action": "do",
"day": "d1",
"first_step": "Revise the homepage mockups for Kite Labs.",
"message": "",
"why": "The Kite Labs homepage mockups are urgent, 1 day late, and take 150 minutes."
},
{
"id": "t7",
"action": "do",
"day": "d2",
"first_step": "Back up the work drive.",
"message": "",
"why": "Backing up the work drive is 8 days late and takes 30 minutes."
},
{
"id": "t8",
"action": "ask",
"day": "",
"first_step": "",
"message": "Sam, is the font licence renewal still needed now that Kite Labs might buy it for the project?",
"why": "Renewing the font licence is 2 days late at 15 minutes, but Kite Labs might buy it."
},
{
"id": "t9",
"action": "do",
"day": "d3",
"first_step": "Write the case study draft.",
"message": "",
"why": "Writing the case study draft is 12 days late and takes 90 minutes."
}
],
"note": "Three tasks land today, Wed 28 Oct, filling all 180 minutes; three sit on Thu 29 Oct and two on Fri 30 Oct, with one question for Sam."
}
The plan the page trusts (ReplyKit.trusted), each task to its day or its tray:
t1Send October invoice to Harbor Studio (20 min, 5 days late, urgent) → Wed 28 Oct (today) - first step: Send the October invoice to Harbor Studio.t2Export final logo files for Fennel Bakery (1 h, 6 days late) → Fri 30 Oct - first step: Export the final logo files for Fennel Bakery.t3Reply to Ola about the brochure proofs (15 min, 4 days late) → Thu 29 Oct - first step: Reply to Ola about the brochure proofs.t4Update portfolio with the museum project (2 h, 19 days late) → Thu 29 Oct - first step: Update the portfolio with the museum project.t5Book accountant call (10 min, 7 days late) → Wed 28 Oct (today) - first step: Book the accountant call.t6Revise homepage mockups for Kite Labs (2 h 30 min, 1 day late, urgent) → Wed 28 Oct (today) - first step: Revise the homepage mockups for Kite Labs.t7Back up the work drive (30 min, 8 days late) → Thu 29 Oct - first step: Back up the work drive.t8Renew font licence (15 min, 2 days late) → ask first - message: "Sam, is the font licence renewal still needed now that Kite Labs might buy it for the project?"t9Write case study draft (1 h 30 min, 12 days late) → Fri 30 Oct - first step: Write the case study draft.
Minutes per day: Wed 28 Oct (today) 180 of 180 min; Thu 29 Oct 165 of 180 min; Fri 30 Oct 150 of 180 min.
The verdict: clears - "The pile clears"; it agrees with the trusted plan, where no task is left for later.
The note: "Three tasks land today, Wed 28 Oct, filling all 180 minutes; three sit on Thu 29 Oct and two on Fri 30 Oct, with one question for Sam."
The page's check (ReplyKit.check): kept.
No notes.
A team lead's backlog
The form: Team lead, two hours a day left between meetings until Friday; today Wed 28 Oct 2026, 2 h of task work a day over the next 3 working days (Mon-Fri). The list as typed:
Approve Q4 hiring plan | Oct 24 | 45 min | urgent
Write performance reviews for Dana and Luis | Oct 17 | 4h | HR wants them this week
Review Marco's architecture doc | Oct 22 | 1h
Update onboarding wiki | Oct 3 | 2h | Priya offered to take this
Prepare budget variance summary | Oct 23 | 1h30 | !
Reply to vendor about licence renewal | Oct 27 | 20 min
Plan team offsite agenda | Oct 10 | 2h | offsite may move to January
Finish security training | Oct 20 | 40 min
Draft Q1 roadmap one-pager | Oct 21 | 3h
Clean up the Jira board | Oct 14 | 1h | Sam usually does this
Send interview feedback for the backend candidate | Oct 26 | 15 min | !
The late tasks as the page reads them, with how late each is (11 tasks, 990 minutes of work; 360 minutes of room over 3 days; left out: none), and the plan days (PileKit.buildInput):
t1. Approve Q4 hiring plan | due Sat 24 Oct, 4 days late | 45 min | urgent
t2. Write performance reviews for Dana and Luis | due Sat 17 Oct, 11 days late | 240 min | notes: HR wants them this week
t3. Review Marco's architecture doc | due Thu 22 Oct, 6 days late | 60 min
t4. Update onboarding wiki | due Sat 3 Oct, 25 days late | 120 min | notes: Priya offered to take this
t5. Prepare budget variance summary | due Fri 23 Oct, 5 days late | 90 min | urgent
t6. Reply to vendor about licence renewal | due Tue 27 Oct, 1 day late | 20 min
t7. Plan team offsite agenda | due Sat 10 Oct, 18 days late | 120 min | notes: offsite may move to January
t8. Finish security training | due Tue 20 Oct, 8 days late | 40 min
t9. Draft Q1 roadmap one-pager | due Wed 21 Oct, 7 days late | 180 min
t10. Clean up the Jira board | due Wed 14 Oct, 14 days late | 60 min | notes: Sam usually does this
t11. Send interview feedback for the backend candidate | due Mon 26 Oct, 2 days late | 15 min | urgent
d1. Wed 28 Oct (today) - 120 min
d2. Thu 29 Oct - 120 min
d3. Fri 30 Oct - 120 min
The page's plain plan, made with no run (ReplyKit.plain(form, ReplyKit.fromPlan(built))):
Wed 28 Oct (today)
[ ] Prepare budget variance summary (90 min)
[ ] Send interview feedback for the backend candidate (15 min)
Thu 29 Oct
[ ] Approve Q4 hiring plan (45 min)
[ ] Clean up the Jira board (60 min)
Fri 30 Oct
[ ] Update onboarding wiki (120 min)
Later
- Write performance reviews for Dana and Luis
- Review Marco's architecture doc
- Reply to vendor about licence renewal
- Plan team offsite agenda
- Finish security training
- Draft Q1 roadmap one-pager
Input:
{
"task": "triage",
"context": "Team lead, two hours a day left between meetings until Friday",
"today": "Wed 28 Oct 2026",
"capacity": "120 minutes a day",
"plan_days": "d1. Wed 28 Oct (today) - 120 min\nd2. Thu 29 Oct - 120 min\nd3. Fri 30 Oct - 120 min",
"tasks": "t1. Approve Q4 hiring plan | due Sat 24 Oct, 4 days late | 45 min | urgent\nt2. Write performance reviews for Dana and Luis | due Sat 17 Oct, 11 days late | 240 min | notes: HR wants them this week\nt3. Review Marco's architecture doc | due Thu 22 Oct, 6 days late | 60 min\nt4. Update onboarding wiki | due Sat 3 Oct, 25 days late | 120 min | notes: Priya offered to take this\nt5. Prepare budget variance summary | due Fri 23 Oct, 5 days late | 90 min | urgent\nt6. Reply to vendor about licence renewal | due Tue 27 Oct, 1 day late | 20 min\nt7. Plan team offsite agenda | due Sat 10 Oct, 18 days late | 120 min | notes: offsite may move to January\nt8. Finish security training | due Tue 20 Oct, 8 days late | 40 min\nt9. Draft Q1 roadmap one-pager | due Wed 21 Oct, 7 days late | 180 min\nt10. Clean up the Jira board | due Wed 14 Oct, 14 days late | 60 min | notes: Sam usually does this\nt11. Send interview feedback for the backend candidate | due Mon 26 Oct, 2 days late | 15 min | urgent",
"totals": "11 tasks, 990 minutes of work; 360 minutes of room over 3 days",
"page_plan": "d1: t5, t11 (105 of 120 min)\nd2: t1, t10 (105 of 120 min)\nd3: t4 (120 of 120 min)\nlater: t7, t2, t8, t9, t3, t6",
"not_sent": "none"
}
Reply:
{
"task": "triage",
"verdict": "overloaded",
"actions": [
{
"id": "t1",
"action": "do",
"day": "d2",
"first_step": "Approve Q4 hiring plan",
"message": "",
"why": "It is urgent and 4 days late, needing 45 minutes."
},
{
"id": "t2",
"action": "do",
"day": "d3",
"first_step": "Write performance reviews for Dana and Luis",
"message": "",
"why": "HR wants these done this week, and it is 11 days late."
},
{
"id": "t3",
"action": "later",
"day": "",
"first_step": "",
"message": "",
"why": "It is 6 days late, needs 60 minutes, and no day has enough room left."
},
{
"id": "t4",
"action": "hand_off",
"day": "",
"first_step": "",
"message": "Priya, could you take on updating the onboarding wiki, due Sat 3 Oct?",
"why": "Priya offered to take this on, and it is 25 days late."
},
{
"id": "t5",
"action": "do",
"day": "d1",
"first_step": "Prepare budget variance summary",
"message": "",
"why": "It is urgent and 5 days late, needing 90 minutes."
},
{
"id": "t6",
"action": "do",
"day": "d2",
"first_step": "Reply to vendor about licence renewal",
"message": "",
"why": "It is 1 day late and takes 20 minutes."
},
{
"id": "t7",
"action": "ask",
"day": "",
"first_step": "",
"message": "Is the team offsite agenda still needed, since the offsite may move to January?",
"why": "Its notes say the offsite may move to January, so it may no longer be needed."
},
{
"id": "t8",
"action": "do",
"day": "d2",
"first_step": "Finish security training",
"message": "",
"why": "It is 8 days late and needs 40 minutes."
},
{
"id": "t9",
"action": "later",
"day": "",
"first_step": "",
"message": "",
"why": "It is 7 days late and needs 180 minutes, more than the remaining room."
},
{
"id": "t10",
"action": "hand_off",
"day": "",
"first_step": "",
"message": "Sam, could you take on cleaning up the Jira board, due Wed 14 Oct?",
"why": "Sam usually does this, and it is 14 days late."
},
{
"id": "t11",
"action": "do",
"day": "d1",
"first_step": "Send interview feedback for the backend candidate",
"message": "",
"why": "It is urgent and 2 days late, taking 15 minutes."
}
],
"note": "Two tasks land today for 105 minutes, three more on Thursday 29 Oct, one long task alone on Friday 30 Oct, and two tasks wait because the three days don't have enough room."
}
The plan the page trusts (ReplyKit.trusted), each task to its day or its tray:
t1Approve Q4 hiring plan (45 min, 4 days late, urgent) → Thu 29 Oct - first step: Approve Q4 hiring plant2Write performance reviews for Dana and Luis (4 h, 11 days late) → Fri 30 Oct - first step: Write performance reviews for Dana and Luist3Review Marco's architecture doc (1 h, 6 days late) → latert4Update onboarding wiki (2 h, 25 days late) → hand off - message: "Priya, could you take on updating the onboarding wiki, due Sat 3 Oct?"t5Prepare budget variance summary (1 h 30 min, 5 days late, urgent) → Wed 28 Oct (today) - first step: Prepare budget variance summaryt6Reply to vendor about licence renewal (20 min, 1 day late) → Thu 29 Oct - first step: Reply to vendor about licence renewalt7Plan team offsite agenda (2 h, 18 days late) → ask first - message: "Is the team offsite agenda still needed, since the offsite may move to January?"t8Finish security training (40 min, 8 days late) → Thu 29 Oct - first step: Finish security trainingt9Draft Q1 roadmap one-pager (3 h, 7 days late) → latert10Clean up the Jira board (1 h, 14 days late) → hand off - message: "Sam, could you take on cleaning up the Jira board, due Wed 14 Oct?"t11Send interview feedback for the backend candidate (15 min, 2 days late, urgent) → Wed 28 Oct (today) - first step: Send interview feedback for the backend candidate
Minutes per day: Wed 28 Oct (today) 105 of 120 min; Thu 29 Oct 105 of 120 min; Fri 30 Oct 240 of 120 min (one task longer than a day, alone).
The verdict: overloaded - "More than the days can hold"; it agrees with the trusted plan, where 2 tasks are left for later.
The note: "Two tasks land today for 105 minutes, three more on Thursday 29 Oct, one long task alone on Friday 30 Oct, and two tasks wait because the three days don't have enough room."
The page's check (ReplyKit.check): kept.
No notes.
Household admin
The form: Household admin that piled up; today Wed 28 Oct 2026, 1 h of task work a day over the next 7 working days (every day). The list as typed:
Pay council tax | Oct 25 | 10 min | urgent
Return the jacket parcel | Oct 18 | 20 min
Book dentist check-up | Sep 30 | 10 min
Reply to landlord about the boiler service | Oct 26 | 15 min
Renew car insurance | Oct 27 | 45 min | !
Sort the receipts drawer | Oct 5 | 1h | Alex offered to do it this weekend
Fix the bike puncture | Oct 22 | 40 min
Order birthday present for Mum | Oct 24 | 20 min | party is Nov 2
Cancel the old gym membership | Oct 12 | 15 min | it may have lapsed already
Water the balcony plants
The late tasks as the page reads them, with how late each is (9 tasks, 235 minutes of work; 420 minutes of room over 7 days; left out: 1 with no due date), and the plan days (PileKit.buildInput):
t1. Pay council tax | due Sun 25 Oct, 3 days late | 10 min | urgent
t2. Return the jacket parcel | due Sun 18 Oct, 10 days late | 20 min
t3. Book dentist check-up | due Wed 30 Sep, 28 days late | 10 min
t4. Reply to landlord about the boiler service | due Mon 26 Oct, 2 days late | 15 min
t5. Renew car insurance | due Tue 27 Oct, 1 day late | 45 min | urgent
t6. Sort the receipts drawer | due Mon 5 Oct, 23 days late | 60 min | notes: Alex offered to do it this weekend
t7. Fix the bike puncture | due Thu 22 Oct, 6 days late | 40 min
t8. Order birthday present for Mum | due Sat 24 Oct, 4 days late | 20 min | notes: party is Nov 2
t9. Cancel the old gym membership | due Mon 12 Oct, 16 days late | 15 min | notes: it may have lapsed already
d1. Wed 28 Oct (today) - 60 min
d2. Thu 29 Oct - 60 min
d3. Fri 30 Oct - 60 min
d4. Sat 31 Oct - 60 min
d5. Sun 1 Nov - 60 min
d6. Mon 2 Nov - 60 min
d7. Tue 3 Nov - 60 min
The page warns: "1 with no date - left out of the plan."
The page's plain plan, made with no run (ReplyKit.plain(form, ReplyKit.fromPlan(built))):
Wed 28 Oct (today)
[ ] Pay council tax (10 min)
[ ] Renew car insurance (45 min)
Thu 29 Oct
[ ] Return the jacket parcel (20 min)
[ ] Book dentist check-up (10 min)
[ ] Reply to landlord about the boiler service (15 min)
[ ] Cancel the old gym membership (15 min)
Fri 30 Oct
[ ] Sort the receipts drawer (60 min)
Sat 31 Oct
[ ] Fix the bike puncture (40 min)
[ ] Order birthday present for Mum (20 min)
Input:
{
"task": "triage",
"context": "Household admin that piled up",
"today": "Wed 28 Oct 2026",
"capacity": "60 minutes a day",
"plan_days": "d1. Wed 28 Oct (today) - 60 min\nd2. Thu 29 Oct - 60 min\nd3. Fri 30 Oct - 60 min\nd4. Sat 31 Oct - 60 min\nd5. Sun 1 Nov - 60 min\nd6. Mon 2 Nov - 60 min\nd7. Tue 3 Nov - 60 min",
"tasks": "t1. Pay council tax | due Sun 25 Oct, 3 days late | 10 min | urgent\nt2. Return the jacket parcel | due Sun 18 Oct, 10 days late | 20 min\nt3. Book dentist check-up | due Wed 30 Sep, 28 days late | 10 min\nt4. Reply to landlord about the boiler service | due Mon 26 Oct, 2 days late | 15 min\nt5. Renew car insurance | due Tue 27 Oct, 1 day late | 45 min | urgent\nt6. Sort the receipts drawer | due Mon 5 Oct, 23 days late | 60 min | notes: Alex offered to do it this weekend\nt7. Fix the bike puncture | due Thu 22 Oct, 6 days late | 40 min\nt8. Order birthday present for Mum | due Sat 24 Oct, 4 days late | 20 min | notes: party is Nov 2\nt9. Cancel the old gym membership | due Mon 12 Oct, 16 days late | 15 min | notes: it may have lapsed already",
"totals": "9 tasks, 235 minutes of work; 420 minutes of room over 7 days",
"page_plan": "d1: t1, t5 (55 of 60 min)\nd2: t3, t9, t2, t4 (60 of 60 min)\nd3: t6 (60 of 60 min)\nd4: t7, t8 (60 of 60 min)\nd5: nothing (0 of 60 min)\nd6: nothing (0 of 60 min)\nd7: nothing (0 of 60 min)\nlater: none",
"not_sent": "1 with no due date"
}
Reply:
{
"task": "triage",
"verdict": "clears",
"actions": [
{
"id": "t1",
"action": "do",
"day": "d1",
"first_step": "Pay the council tax.",
"message": "",
"why": "Paying the council tax is urgent and 3 days late, taking 10 minutes."
},
{
"id": "t2",
"action": "do",
"day": "d2",
"first_step": "Return the jacket parcel.",
"message": "",
"why": "Returning the jacket parcel is 10 days late and takes 20 minutes."
},
{
"id": "t3",
"action": "do",
"day": "d2",
"first_step": "Book the dentist check-up.",
"message": "",
"why": "Booking the dentist check-up is 28 days late and only takes 10 minutes."
},
{
"id": "t4",
"action": "do",
"day": "d3",
"first_step": "Reply to the landlord about the boiler service.",
"message": "",
"why": "Replying to the landlord about the boiler service is 2 days late and takes 15 minutes."
},
{
"id": "t5",
"action": "do",
"day": "d1",
"first_step": "Renew the car insurance.",
"message": "",
"why": "Renewing the car insurance is urgent and 1 day late, needing 45 minutes."
},
{
"id": "t6",
"action": "hand_off",
"day": "",
"first_step": "",
"message": "Alex, could you take on sorting the receipts drawer, due Mon 5 Oct, since you offered to help this weekend?",
"why": "Alex offered to sort the receipts drawer, which is 23 days late."
},
{
"id": "t7",
"action": "do",
"day": "d3",
"first_step": "Fix the bike puncture.",
"message": "",
"why": "Fixing the bike puncture is 6 days late and takes 40 minutes."
},
{
"id": "t8",
"action": "do",
"day": "d2",
"first_step": "Order the birthday present for Mum.",
"message": "",
"why": "Ordering Mum's birthday present is 4 days late and the party is Nov 2."
},
{
"id": "t9",
"action": "ask",
"day": "",
"first_step": "",
"message": "Is cancelling the old gym membership still needed, given it may have lapsed already?",
"why": "The old gym membership is 16 days late and may have lapsed already."
}
],
"note": "Tasks are done on Wed 28 Oct, Thu 29 Oct and Fri 30 Oct, with 55 of today's minutes used and 160 minutes planned in total."
}
The plan the page trusts (ReplyKit.trusted), each task to its day or its tray:
t1Pay council tax (10 min, 3 days late, urgent) → Wed 28 Oct (today) - first step: Pay the council tax.t2Return the jacket parcel (20 min, 10 days late) → Thu 29 Oct - first step: Return the jacket parcel.t3Book dentist check-up (10 min, 28 days late) → Thu 29 Oct - first step: Book the dentist check-up.t4Reply to landlord about the boiler service (15 min, 2 days late) → Fri 30 Oct - first step: Reply to the landlord about the boiler service.t5Renew car insurance (45 min, 1 day late, urgent) → Wed 28 Oct (today) - first step: Renew the car insurance.t6Sort the receipts drawer (1 h, 23 days late) → hand off - message: "Alex, could you take on sorting the receipts drawer, due Mon 5 Oct, since you offered to help this weekend?"t7Fix the bike puncture (40 min, 6 days late) → Fri 30 Oct - first step: Fix the bike puncture.t8Order birthday present for Mum (20 min, 4 days late) → Thu 29 Oct - first step: Order the birthday present for Mum.t9Cancel the old gym membership (15 min, 16 days late) → ask first - message: "Is cancelling the old gym membership still needed, given it may have lapsed already?"
Minutes per day: Wed 28 Oct (today) 55 of 60 min; Thu 29 Oct 50 of 60 min; Fri 30 Oct 55 of 60 min.
The verdict: clears - "The pile clears"; it agrees with the trusted plan, where no task is left for later.
The note: "Tasks are done on Wed 28 Oct, Thu 29 Oct and Fri 30 Oct, with 55 of today's minutes used and 160 minutes planned in total."
The page's check (ReplyKit.check): kept.
No notes.
The CSV for the first sample
What ReplyKit.csv(built, ReplyKit.trusted(ReplyKit.normalize(reply), built)) writes for "Back from a week off" above: a header and one row per task in id order, 9 rows (lines end in CRLF; a cell with a comma or a quote is quoted).
id,task,due,days_late,minutes,urgent,action,day,first_step,message
t1,Send October invoice to Harbor Studio,2026-10-23,5,20,yes,do,2026-10-28,Send the October invoice to Harbor Studio.,
t2,Export final logo files for Fennel Bakery,2026-10-22,6,60,,do,2026-10-30,Export the final logo files for Fennel Bakery.,
t3,Reply to Ola about the brochure proofs,2026-10-24,4,15,,do,2026-10-29,Reply to Ola about the brochure proofs.,
t4,Update portfolio with the museum project,2026-10-09,19,120,,do,2026-10-29,Update the portfolio with the museum project.,
t5,Book accountant call,2026-10-21,7,10,,do,2026-10-28,Book the accountant call.,
t6,Revise homepage mockups for Kite Labs,2026-10-27,1,150,yes,do,2026-10-28,Revise the homepage mockups for Kite Labs.,
t7,Back up the work drive,2026-10-20,8,30,,do,2026-10-29,Back up the work drive.,
t8,Renew font licence,2026-10-26,2,15,,ask first,,,"Sam, is the font licence renewal still needed now that Kite Labs might buy it for the project?"
t9,Write case study draft,2026-10-16,12,90,,do,2026-10-30,Write the case study draft.,
Build the input yourself
Load the engine in node (global.window = global; require("./taskkit.js"); require("./replykit.js")), then:
const K = PileKit, R = ReplyKit;
const form = K.form({ tasks: "Send invoice to Harbor Studio | Oct 23 | 20 min | !\nBook accountant call | Oct 21 | 10 min",
today: "2026-10-28", capacity: "1", week: "mon-fri", horizon: "3", context: "" });
// form.tasks is the late list as read: [{id, title, due, late, mins, assumed, high, notes}], dates as whole-day numbers
const built = K.buildInput(form); // built.ok is false, with built.note, until a task is late or due today
// built.plan: the plain plan {place, used, later}; built.warn: what the page warns about
R.plain(form, R.fromPlan(built)) // the plain plan as a checklist, no run needed
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// const t = R.trusted(reply, built) // {acts, dropped, missing, load, later}: one action per task the page sent
// R.plain(form, t) // "Wed 28 Oct (today)" then " [ ] Send invoice to Harbor Studio (20 min)" ...
// R.check(reply, built) // {status: kept or flagged, notes}
// R.csv(built, t) // the CSV, one row per task
Form keys: tasks (one task per line), today (YYYY-MM-DD, or empty for the device's own date), capacity (hours of task work a day, 0.5 to 10, rounded to the quarter hour), week (mon-fri, sun-thu, mon-sat or every), horizon (3, 5, 7 or 10 working days) and context (what is going on, or empty).
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://late-pile.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://late-pile.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// late-pile.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://late-pile.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://late-pile.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// LatePile.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class LatePile {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://late-pile.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://late-pile.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://late-pile.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://late-pile.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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":"late-pile"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "late-pile"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "late-pile" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "late-pile"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "late-pile"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "late-pile" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "late-pile"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "late-pile" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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": "triage", "context": "none", "today": "Wed 28 Oct 2026", "capacity": "60 minutes a day", "plan_days": "d1. Wed 28 Oct (today) - 60 min\nd2. Thu 29 Oct - 60 min\nd3. Fri 30 Oct - 60 min", "tasks": "t1. Send invoice to Harbor Studio | due Fri 23 Oct, 5 days late | 20 min | urgent\nt2. Book accountant call | due Wed 21 Oct, 7 days late | 10 min", "totals": "2 tasks, 30 minutes of work; 180 minutes of room over 3 days", "page_plan": "d1: t1, t2 (30 of 60 min)\nd2: nothing (0 of 60 min)\nd3: nothing (0 of 60 min)\nlater: none", "not_sent": "none"}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "triage", "context": "none", "today": "Wed 28 Oct 2026", "capacity": "60 minutes a day", "plan_days": "d1. Wed 28 Oct (today) - 60 min\nd2. Thu 29 Oct - 60 min\nd3. Fri 30 Oct - 60 min", "tasks": "t1. Send invoice to Harbor Studio | due Fri 23 Oct, 5 days late | 20 min | urgent\nt2. Book accountant call | due Wed 21 Oct, 7 days late | 10 min", "totals": "2 tasks, 30 minutes of work; 180 minutes of room over 3 days", "page_plan": "d1: t1, t2 (30 of 60 min)\nd2: nothing (0 of 60 min)\nd3: nothing (0 of 60 min)\nlater: none", "not_sent": "none"}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "triage", context, today, capacity, plan_days, tasks, totals, page_plan, not_sent} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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 late-pile:<task>:<inputHash>:a<attempt> (the one task is triage), 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="late-pile:triage:$(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}'
jq -r '.verdict, (.actions[] | "\(.id) \(.action) \(.day)")' reply.json
import hashlib, time
key = "late-pile:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
for a in reply.get("actions") or []:
print(a["id"], a["action"], a.get("day", ""))
import { createHash } from "node:crypto";
const key = `late-pile:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
for (const a of reply.actions || []) console.log(a.id, a.action, a.day);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "late-pile:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
actions, _ := reply["actions"].([]any)
fmt.Println(j["charged_credits"], j["truncated"], reply["verdict"])
for _, x := range actions {
a := x.(map[string]any)
fmt.Println(a["id"], a["action"], a["day"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "late-pile:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.get("verdict"));
for (JsonNode a : reply.path("actions")) System.out.println(a.path("id").asText() + " " + a.path("action").asText() + " " + a.path("day").asText());
require "digest"
key = "late-pile:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
(reply["actions"] || []).each { |a| puts "#{a["id"]} #{a["action"]} #{a["day"]}" }
$key = "late-pile:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), "\n";
foreach ($reply["actions"] ?? [] as $a) echo $a["id"], " ", $a["action"], " ", $a["day"] ?? "", "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"late-pile:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply!["verdict"]}");
foreach (var a in reply["actions"] as JsonArray ?? new JsonArray()) Console.WriteLine($"{a!["id"]} {a["action"]} {a["day"]}");
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: late-pile:triage:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "triage", "context": "none", "today": "Wed 28 Oct 2026", "capacity": "60 minutes a day", "plan_days": "d1. Wed 28 Oct (today) - 60 min\nd2. Thu 29 Oct - 60 min\nd3. Fri 30 Oct - 60 min", "tasks": "t1. Send invoice to Harbor Studio | due Fri 23 Oct, 5 days late | 20 min | urgent\nt2. Book accountant call | due Wed 21 Oct, 7 days late | 10 min", "totals": "2 tasks, 30 minutes of work; 180 minutes of room over 3 days", "page_plan": "d1: t1, t2 (30 of 60 min)\nd2: nothing (0 of 60 min)\nd3: nothing (0 of 60 min)\nlater: none", "not_sent": "none"}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "late-pile:triage:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `late-pile:triage:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "late-pile:triage:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "late-pile:triage:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "late-pile:triage:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: late-pile:triage:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"late-pile:triage:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);