← Next Thing
API
Tokens

Run Next Thing from your own code

The letter you keep putting off, as one next action. Next Thing takes a letter, an email or a set of instructions someone has to act on - a council form, a school email, a first-week checklist - and shapes it for an ADHD reader the way the source skill says: lead with the next action, number the steps, give minutes instead of "a bit of work", park the tangents, restate where things stand, make finished work visible. The model never invents a date, an amount or a number - every date is a calendar day the page found, and every amount, phone number, link and reference is copied from the text.

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page does the reading for free - it resolves every date to a calendar day (weekdays, "next Friday" and "within 21 days of the date of this letter" counted from the right date), marks each one a deadline, an appointment or a plain date with the days left, finds every ask with a time estimate, what to bring, the amounts, phone numbers, emails, links and reference numbers, and runs the NT- findings (a passed deadline, one within three days, a date that reads two ways, a wall of text, a buried first ask). That is ntscan.js. The run either writes the action card from the text ("plan") or restates the plan after some steps are done ("update"); the page then holds the reply to its sources (ntreply.js): a next action of five minutes or less, timed steps that quote the text, every date one the page found, every deadline carried, every ask a step or parked, every error and warning answered.

Base URL and the response shape

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

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

The token is minted for this app (the guest call takes {"slug":"next-thing"}), 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 field the run needs (task, text, today, facts) is missing or not a string. Send every field as a string; minutes and progress may be "".
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: both lanes are metered and need a personal sign-in.
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 comes first

Two lanes - "plan" (the text, as pasted, turned into the action card: a one-line summary, one next action of five minutes or less, at most seven numbered steps each with minutes, a due date and a quote from the text, the deadlines, what to have ready, what is parked under later, at most one question and a last line starting "Next:") and "update" (the same text plus progress - the plan so far with its ticked steps and a note of what happened - turned into the restated card: a state line "Step N of M done: ... Next: ...", the wins, and every step again with a status). Both lanes send the same fields; progress is "" for "plan". A task the model does not know is answered as plan, with a caveat saying so.

The input

Every field is a string.

FieldRequiredWhat
taskyes"plan" or "update". The model answers that task and only that task.
textyesthe letter, email or instructions, as pasted (line endings normalised to \n, trimmed). The page sends up to 12,000 characters - the letter itself, not the whole thread - and at least a sentence or two.
todayyestoday's date, YYYY-MM-DD. Weekdays and "tomorrow" are counted from it, and the days left on each deadline.
minutesnohow many minutes the reader has right now, digits only ("20"); the first steps should fit in them and the summary says what fits. Send "" when there is none.
progressupdate onlya JSON string (not an object): the plan so far and what happened - see the progress format. Send "" for "plan".
factsyesthe page's reading of the same text, built in the browser by ntscan.js (NtScan.facts(), via buildInput) - see the facts format.
retry_notenopresent only on a reformat retry: the previous reply could not be read as one JSON object, and this says how to answer again. Leave it out otherwise.

The progress format

For "update", progress carries the steps of the earlier card (each action, minutes - a number, by - a YYYY-MM-DD date or "", and done - a boolean) and note, what happened since in the reader's words. The page builds it with NtScan.progressString(steps, note) and serialises it with JSON.stringify, so it travels as one escaped string. It needs at least one step, and at least one step ticked or a note written; the page keeps 12 steps and 1,500 characters of note.

{"steps": [
   {"action": "Sign your contract in DocuSign.", "minutes": 10, "by": "2026-11-01", "done": true},
   {"action": "Complete the new starter form at https://people.quillmere.example.com/starter with your bank details and tax code.", "minutes": 25, "by": "", "done": false}
 ],
 "note": "Signed the contract on Saturday. I cannot find my tax code anywhere."}

The facts format

facts is the page's own reading, written as plain text. The page builds it in the browser with ntscan.js (NtScan.buildInput(state, task) calls NtScan.facts(scan, task, state)), and the model may use only the dates it prints: every by and every deadline is one of its calendar days, the card answers every error and warn rule id it lists, and the page then holds the reply to the same text. So an API client should send facts built the same way - from ntscan.js run under node (Build the input yourself; the same text and today give the same facts as the page), or the text the page builds for the same letter - not an empty string and not dates of your own.

Today: <YYYY-MM-DD> (<weekday>). The letter is dated <YYYY-MM-DD>.   (or: No letter date found.)
Time the reader has now: <n> minutes.                               (only when minutes is given)
Read: <n> words, about <n> minutes to read; longest paragraph <n> words.
Dates (text | calendar day | kind | days from today):
- <as written> | <YYYY-MM-DD> (<weekday>) | <deadline|appointment|date> | <n>[ (passed)][ | counted from <date>][ | reads two ways]
Asks the page found (n | verb | page estimate | by | sentence):
- <n> | <verb> | <n> min | <YYYY-MM-DD|-> | <the sentence>
To bring, attach or send: ... .   Amounts: ... .   Contacts and references: phone ...; email ...; link ...; Reference ... .
Findings (rule | severity | line | where | text):
- <rule id> | <error|warn|info> | line <n> | <where|-> | <title>[ (note)]
The plan so far (<n> of <m> steps done):                           (update only)
- step <n> | <done|to do> | <action> | <n> min[ | by <YYYY-MM-DD>]
What happened since: <note|(nothing written)>

A list with nothing in it gets the single line - none; the bring, amounts and contacts lines are left out when empty. A real one - the facts the page sends for its "council letter" sample on the "plan" lane (the council, the reader and every number are invented):

Today: 2026-10-30 (friday). The letter is dated 2026-10-21.
Time the reader has now: 20 minutes.
Read: 222 words, about 1 minute to read; longest paragraph 91 words.
Dates (text | calendar day | kind | days from today):
- 1 December 2026 | 2026-12-01 (tuesday) | date | 32
- within 21 days | 2026-11-11 (wednesday) | deadline | 12 | counted from 2026-10-21
Asks the page found (n | verb | page estimate | by | sentence):
- 1 | complete | 30 min | 2026-11-11 | Please complete the enclosed review form and return it to us within 21 days of the date of this letter.
- 2 | include | 10 min | - | You must include your last two payslips or, if you are self-employed, your most recent accounts, and a bank statement for every account held by you or your partner covering the last two months.
- 3 | tell | 5 min | - | If anyone has moved into or out of your home since your last claim, please tell us their name and the date they moved.
- 4 | call | 20 min | - | If you have questions please call us on 0306 999 0123, Monday to Friday 9am to 5pm, quoting your reference number.
To bring, attach or send: last two payslips; most recent accounts; bank statement.
Amounts: GBP 1,842.60.
Contacts and references: phone 0306 999 0123; link www.harrowmere.example.org/ctr-upload; Reference CTR-448812.
Findings (rule | severity | line | where | text):
- NT-RELATIVE | info | line 9 | within 21 days | A deadline counted from another date (counted from the letter's date, 2026-10-21: Wed 11 Nov 2026)
- NT-COND | info | line 11 | tell | An ask that applies only if something is true (ask 3: If anyone has moved into or out of your home since your last claim)
- NT-LONGSENT | info | line 11 | - | Long sentences (1 sentence over 35 words)
- NT-COND | info | line 13 | call | An ask that applies only if something is true (ask 4: If you have questions please call us on 0306 999 0123)

Rule ids the page uses, with their severity and area:

RuleSeverityAreaTitle
NT-PASTerrordatesA deadline has already passed
NT-SOONwarndatesA deadline is within three days
NT-AMBIGDATEwarndatesA date that reads two ways
NT-NOYEARinfodatesDates without a year
NT-WEEKDAYwarndatesThe weekday and the date disagree
NT-RELATIVEinfodatesA deadline counted from another date
NT-VAGUEinfodatesA vague time with no date
NT-WALLwarnreadingA wall of text
NT-LONGSENTinforeadingLong sentences
NT-BURIEDwarnactionsThe first ask is buried
NT-MANYwarnactionsMore than five things to do
NT-NOACTIONinfoactionsNo clear ask found
NT-CONDinfoactionsAn ask that applies only if something is true
NT-NOCONTACTinfocontactsAsked to call or write, but no number or address

A point the page cannot see (posting takes days, a partner has to supply statements too) comes back as a finding with the rule NEW.

The standard the model applies

The source skill - @ayghri/i-have-adhd (ayghri/i-have-adhd, MIT) - is the only standard. Its five facts about how an ADHD reader reads:

House rules

The replies

Both lanes carry task, summary, next_action, steps, deadlines, bring, later, question, last_line, findings, prescan and caveats; "update" adds state, wins and a status on every step. The shapes below are what the page reads after NtReply.normalize(): a missing list comes back empty, minutes becomes a whole number (0 when absent), a next_action sent as a bare string becomes {text, minutes: 0}, an unknown severity becomes medium, a call other than disagree becomes agree, an unknown status becomes todo, and steps with no action are dropped. A reply is usable when it has a next action and at least one step.

plan

The action card, written from the text and the facts.

  1. summary - one line: what this text is about and the one date that matters most (and, with minutes, what fits in them).
  2. next_action - text and minutes (five or fewer).
  3. steps - at most seven, in order, each action, minutes, by (a page date or ""), source (a quote from the text) and needs (short words from the text).
  4. deadlines - each date and what; bring - five items or fewer, from the text; later - each item and why.
  5. question; last_line; findings (points the page cannot see: rule NEW, severity high / medium / low, title, where, why, fix); prescan (one entry per distinct error or warn rule id in the facts: rule, call, note); caveats (strings).
{
  "task": "plan",
  "summary": "",
  "next_action": {"text": "", "minutes": 2},
  "steps": [{"action": "", "minutes": 10, "by": "", "source": "", "needs": [""]}],
  "deadlines": [{"date": "", "what": ""}],
  "bring": [""],
  "later": [{"item": "", "why": ""}],
  "question": "",
  "last_line": "Next: ",
  "findings": [{"rule": "NEW", "severity": "medium", "title": "", "where": "", "why": "", "fix": ""}],
  "prescan": [{"rule": "NT-SOON", "call": "agree", "note": ""}],
  "caveats": [""]
}

update

The plan restated. The reader cannot hold "we are on step 3 of 5" between visits, so the reply says it.

  1. state - one line, "Step N of M done: <what now works>. Next: <the next step>.", where N counts the steps marked done and M is the number of steps.
  2. wins - what is finished, in concrete terms ("The contract is signed", not "made progress"), one line each.
  3. steps - the whole plan again, in order, each with status: done (every step the progress marks done stays done, worded as before), todo (unchanged), changed (what happened changes it) or new (what happened adds it, quoting the note in source). A step that no longer applies goes in later with why.
  4. next_action - the first thing of the next step that is not done, five minutes or less; summary - one line, where things stand and what fits in minutes.
  5. deadlines, bring (what is still needed), later, question, last_line, findings, prescan, caveats - as for "plan".
{
  "task": "update",
  "state": "Step 1 of 4 done: . Next: .",
  "summary": "",
  "wins": [""],
  "next_action": {"text": "", "minutes": 2},
  "steps": [{"action": "", "minutes": 10, "by": "", "source": "", "needs": [], "status": "done"}],
  "deadlines": [{"date": "", "what": ""}],
  "bring": [],
  "later": [],
  "question": "",
  "last_line": "Next: ",
  "findings": [],
  "prescan": [],
  "caveats": []
}

The page holds either reply to its sources with NtReply.check() and shows each miss next to the card as a note. It wants no preamble, closer, hedge, idiom or vague estimate in what the reader sees; a next action with a number of minutes, five or fewer; seven steps at most, each with minutes and none chaining several actions; a last line; every by and deadline a YYYY-MM-DD date the page found (or today, or the letter's date); every deadline the page found carried in deadlines or a step's by; every amount and phone number found in the text (or the note); every step not yet done quoting the text or the note; every ask the page found covered by a step, a need, bring or later; every error and warning answered by rule id and no rule id the page did not report other than NEW; and at most one question. For "update" it also wants the state line to say "N of M" and match the steps, every ticked step still shown as done, and a win when steps were done.

Reading the reply

The job's output.output is text that should be exactly one JSON object. Parse it; if a model ever wraps it in prose or a fence, take the text from the first { to the last } and parse that (the page does the same in ntreply.js, and closes a stream that stopped mid-object). If it still does not parse, send the same input again with a retry_note such as "Your previous reply was not usable. Reply again with ONLY the JSON object for task 'plan' - no prose, no code fences." and a new attempt number in the key (see step 5). Step 5 prints the next action first, then the numbered steps with minutes and due dates, the deadlines, what is parked under later and the last line - and for "update" the state line and the wins above them, with each step's status.

Plan, then update

The two lanes chain. A "plan" reply's steps become the checklist; once the reader has ticked some and written what happened, the same text and today go to "update" with progress = NtScan.progressString(reply.steps with a done flag each, note) and fresh facts built for the update lane (they add the plan so far and the note). An "update" reply's steps can be fed back the same way for the next round: a step is done when its status is done.

Worked samples

Two of the page's own samples, with the real saved replies it shows for them. Every council, company, person and number is invented.

Sample 1 - plan: the council letter

A Council Tax Reduction review: a form to return "within 21 days of the date of this letter", payslips and bank statements to include, an only-if ask, a phone number and an upload link. The reader has 20 minutes. The page counts the 21 days from the letter's date to 2026-11-11 and reports four info findings and no warnings. Input - text is exactly what the page sends; facts is shortened here (each ... stands for lines left out - send the whole text the page builds, shown in full under the facts format):

{
 "task": "plan",
 "text": "Harrowmere District Council - Revenues and Benefits\nDate: 21 October 2026\n\nCouncil Tax Reduction - review of your claim\nReference number: CTR-448812\n\nDear Ms Adeyemi,\n\nWe are reviewing your Council Tax Reduction claim because our records show that your household circumstances may have changed since your last award. To continue your reduction from 1 December 2026 we need some information from you. Please complete the enclosed review form and return it to us within 21 days of the date of this letter. You must include your last two payslips or, if you are self-employed, your most recent accounts, and a bank statement for every account held by you or your partner covering the last two months.\n\nIf anyone has moved into or out of your home since your last claim, please tell us their name and the date they moved. If you do not return the form by the date above, your reduction may stop and you will receive a revised bill for the full amount, which for your property is GBP 1,842.60 for the year.\n\nYou can upload your documents at www.harrowmere.example.org/ctr-upload or post them to Revenues and Benefits, PO Box 117, Harrowmere WB1 4QT. If you have questions please call us on 0306 999 0123, Monday to Friday 9am to 5pm, quoting your reference number.\n\nYours sincerely,\nBenefits Review Team",
 "today": "2026-10-30",
 "minutes": "20",
 "progress": "",
 "facts": "Today: 2026-10-30 (friday). The letter is dated 2026-10-21.\nTime the reader has now: 20 minutes.\n...\nDates (text | calendar day | kind | days from today):\n- 1 December 2026 | 2026-12-01 (tuesday) | date | 32\n- within 21 days | 2026-11-11 (wednesday) | deadline | 12 | counted from 2026-10-21\nAsks the page found (n | verb | page estimate | by | sentence):\n- 1 | complete | 30 min | 2026-11-11 | Please complete the enclosed review form and return it to us within 21 days of the date of this letter.\n...\nContacts and references: phone 0306 999 0123; link www.harrowmere.example.org/ctr-upload; Reference CTR-448812.\nFindings (rule | severity | line | where | text):\n- NT-RELATIVE | info | line 9 | within 21 days | A deadline counted from another date (counted from the letter's date, 2026-10-21: Wed 11 Nov 2026)\n..."
}

Reply - the saved reply, abbreviated: steps keeps 2 of 4 and later 2 of 4 (the full reply goes on with filling in the form, 30 minutes, and uploading it, 10 minutes, both by 2026-11-11, and also has a fourth bring item and a second NEW finding - the partner's statements are needed too):

{
 "task": "plan",
 "summary": "Council Tax Reduction review (ref CTR-448812): return the form and documents by 2026-11-11. In your 20 minutes, steps 1 and 2 fit: gather payslips and bank statements.",
 "next_action": {"text": "Find the enclosed review form and put it on your desk with this letter.", "minutes": 2},
 "steps": [
  {"action": "Find your last two payslips (or your most recent accounts if self-employed).", "minutes": 10, "by": "2026-11-11", "source": "your last two payslips or, if you are self-employed, your most recent accounts", "needs": ["last two payslips", "most recent accounts"]},
  {"action": "Download a bank statement covering the last two months for every account held by you or your partner.", "minutes": 10, "by": "2026-11-11", "source": "a bank statement for every account held by you or your partner covering the last two months", "needs": ["bank statement"]}
 ],
 "deadlines": [
  {"date": "2026-11-11", "what": "Return the completed review form and documents"},
  {"date": "2026-12-01", "what": "Reduction continues from this date if the review is complete"}
 ],
 "bring": ["review form", "last two payslips (or most recent accounts if self-employed)", "bank statements for the last two months, every account of you and your partner"],
 "later": [
  {"item": "If anyone moved into or out of your home since your last claim, give their name and the date they moved.", "why": "Only applies if someone moved in or out."},
  {"item": "Call 0306 999 0123, Monday to Friday 9am to 5pm, quoting CTR-448812.", "why": "Only needed if you have questions."}
 ],
 "question": "",
 "last_line": "Next: Save 0306 999 0123 in your phone as Council Tax, with CTR-448812 in the notes.",
 "findings": [
  {"rule": "NEW", "severity": "medium", "title": "Posting takes days", "where": "post them to Revenues and Benefits, PO Box 117", "why": "The form has to reach the council by 2026-11-11; a posted form can arrive late.", "fix": "Upload at www.harrowmere.example.org/ctr-upload, or post well before 2026-11-11 and keep proof of postage."}
 ],
 "prescan": [],
 "caveats": ["Payslips or accounts: which one depends on whether you are employed or self-employed."]
}

The next action takes two minutes and needs nothing; the steps are timed (filling in the form keeps the page's 30 minutes) and each quotes the letter; both of the page's dates come back as YYYY-MM-DD, the only-if asks and the phone number are parked under later with why, the amount and the phone number are copied exactly, and with only info findings prescan is empty. The page's reply check finds nothing to note.

Sample 2 - update: a new job, half done

A first-week email from an employer: six asks, two deadlines within three days. The reader has ticked two steps of the earlier card and written what happened: the 9:30 laptop slots were gone, so 11:00 was booked, and the tax code the starter form needs cannot be found. The page reports two warnings (NT-MANY, NT-SOON). Input - text and progress are exactly what the page sends; facts is shortened (send the whole text, about 2,900 characters):

{
 "task": "update",
 "text": "Subject: Welcome to Quillmere Analytics - your first week\n\nHi Sam,\n\nWelcome aboard! We are really happy you are joining us on Monday 2 November. A few things to get through before and during your first week:\n\n- Sign your contract in DocuSign (link sent separately) by Sunday 1 November.\n- Complete the new starter form at https://people.quillmere.example.com/starter so payroll can set you up - we need your bank details and tax code.\n- Bring your passport or birth certificate on Monday for the right-to-work check.\n- Book a 30 minute equipment pickup with IT at https://it.quillmere.example.com/book - laptops go out from 9:30 on Monday.\n- Read the engineering handbook before your Wednesday onboarding session (4 November at 2pm).\n- Set up two-factor authentication on your Google account once you have your laptop.\n\nYour buddy is Ilsa Marrowby (ilsa.marrowby@quillmere.example.com) - she will take you for lunch on Monday. If anything comes up, call me on +44 20 7946 0321.\n\nSee you soon,\nTobin Ashgrove, People Team",
 "today": "2026-10-30",
 "minutes": "45",
 "progress": "{\"steps\":[{\"action\":\"Sign your contract in DocuSign.\",\"minutes\":10,\"by\":\"2026-11-01\",\"done\":true},{\"action\":\"Book a 30 minute equipment pickup slot for Monday at https://it.quillmere.example.com/book (laptops go out from 9:30).\",\"minutes\":5,\"by\":\"2026-11-02\",\"done\":true},{\"action\":\"Complete the new starter form at https://people.quillmere.example.com/starter with your bank details and tax code.\",\"minutes\":25,\"by\":\"\",\"done\":false},{\"action\":\"Put your passport or birth certificate in the bag you take on Monday.\",\"minutes\":10,\"by\":\"2026-11-02\",\"done\":false},{\"action\":\"Set up two-factor authentication on your Google account on the new laptop.\",\"minutes\":15,\"by\":\"\",\"done\":false},{\"action\":\"Read the engineering handbook.\",\"minutes\":60,\"by\":\"2026-11-04\",\"done\":false}],\"note\":\"Signed the contract on Saturday. The 9:30 laptop slots were gone, so I booked 11:00 on Monday instead. I cannot find my tax code anywhere.\"}",
 "facts": "Today: 2026-10-30 (friday). No letter date found.\nTime the reader has now: 45 minutes.\n...\nFindings (rule | severity | line | where | text):\n- NT-MANY | warn | line 0 | - | More than five things to do (6 asks in the text)\n- NT-SOON | warn | line 7 | 1 November | A deadline is within three days (Sun 1 Nov 2026, in 2 days)\n- NT-SOON | warn | line 10 | on Monday | A deadline is within three days (Mon 2 Nov 2026, in 3 days)\n- NT-NOYEAR | info | line 5 | 2 November, 1 November, 4 November | Dates without a year (3 dates read as 2026-11-02, 2026-11-01, 2026-11-04)\nThe plan so far (2 of 6 steps done):\n- step 1 | done | Sign your contract in DocuSign. | 10 min | by 2026-11-01\n- step 2 | done | Book a 30 minute equipment pickup slot for Monday at https://it.quillmere.example.com/book (laptops go out from 9:30). | 5 min | by 2026-11-02\n...\nWhat happened since: Signed the contract on Saturday. The 9:30 laptop slots were gone, so I booked 11:00 on Monday instead. I cannot find my tax code anywhere."
}

Reply - the saved reply, abbreviated: steps keeps 4 of 7 (the two done, the new one and the changed one; the full reply goes on with the passport, the two-factor setup and the handbook, all todo), and the full reply also has a second later item, a second NEW finding and a second caveat:

{
 "task": "update",
 "state": "Step 2 of 7 done: the contract is signed and the laptop pickup is booked. Next: find your tax code.",
 "wins": ["The contract is signed in DocuSign.", "The IT laptop pickup is booked for 11:00 on Monday 2026-11-02."],
 "next_action": {"text": "Open your last payslip or P45 and look for the tax code.", "minutes": 5},
 "steps": [
  {"action": "Sign your contract in DocuSign.", "minutes": 10, "by": "2026-11-01", "source": "Sign your contract in DocuSign (link sent separately) by Sunday 1 November.", "needs": [], "status": "done"},
  {"action": "Book a 30 minute equipment pickup slot for Monday at https://it.quillmere.example.com/book (laptops go out from 9:30).", "minutes": 5, "by": "2026-11-02", "source": "Book a 30 minute equipment pickup with IT at https://it.quillmere.example.com/book", "needs": [], "status": "done"},
  {"action": "Find your tax code on your last payslip, P45 or P60.", "minutes": 15, "by": "", "source": "I cannot find my tax code anywhere.", "needs": ["last payslip, P45 or P60"], "status": "new"},
  {"action": "Complete the new starter form at https://people.quillmere.example.com/starter with your bank details and the tax code you found.", "minutes": 25, "by": "", "source": "Complete the new starter form at https://people.quillmere.example.com/starter so payroll can set you up - we need your bank details and tax code.", "needs": ["bank details", "tax code"], "status": "changed"}
 ],
 "deadlines": [
  {"date": "2026-11-01", "what": "Contract signed in DocuSign (done)"},
  {"date": "2026-11-02", "what": "First day: right-to-work check, laptop pickup at 11:00"},
  {"date": "2026-11-04", "what": "Onboarding session at 2pm; handbook read before it"}
 ],
 "bring": ["passport or birth certificate", "bank details", "tax code"],
 "later": [
  {"item": "If the tax code is not on any payslip, P45 or P60, call Tobin Ashgrove on +44 20 7946 0321 and ask what to put on the starter form.", "why": "Only applies if the tax code cannot be found."}
 ],
 "question": "",
 "last_line": "Next: put your passport or birth certificate in your Monday bag.",
 "findings": [
  {"rule": "NEW", "severity": "medium", "title": "Missing tax code blocks the starter form", "where": "progress note", "why": "The form needs the tax code so payroll can set you up; without it the form stays unfinished.", "fix": "Find the tax code on a payslip, P45 or P60 first; if it is not there, call +44 20 7946 0321."}
 ],
 "prescan": [
  {"rule": "NT-MANY", "call": "agree", "note": "Six asks plus one new step from the note; two are done, five are left."},
  {"rule": "NT-SOON", "call": "agree", "note": "The 2026-11-01 contract deadline is met. The 2026-11-02 items (passport or birth certificate, 11:00 pickup) are in 3 days."}
 ],
 "caveats": ["With 45 minutes now: finding the tax code (15) and the starter form (25) fit, 40 minutes in total."]
}

"Step 2 of 7 done" matches the seven steps, two of them done and worded as before; the missing tax code becomes a new step quoting the note, and the starter form that depends on it is changed. The met deadline stays, marked "(done)"; both warning ids (NT-MANY, NT-SOON, the latter reported twice) are answered once each in prescan, and the info id needs no entry. The page's reply check finds nothing to note.

Build the input yourself

The reading and the reply checker are plain scripts that run under node (global.window = global; require("./ntscan.js"); require("./ntreply.js"), from the app at https://next-thing.skillsafe.ai/ntscan.js and /ntreply.js). The same text and today give the same facts as the page - both samples above reproduce byte for byte:

const S = NtScan, R = NtReply;
const st = { text: fs.readFileSync("letter.txt", "utf8"), today: "2026-10-30", minutes: "20", progress: "" };
const b = S.buildInput(st, "plan");                // or "update" with st.progress; b.ok is false (with b.note) when the text cannot run
console.log(b.scan.deadlines.map(d => d.iso), b.scan.findings.map(f => f.id)); // [ '2026-11-11' ] [ 'NT-RELATIVE', ... ]
const wire = S.toWire(b.input);                    // task, text, today, minutes, progress, facts - all strings; POST this to /estimate and /run
// for update: st.progress = S.progressString(steps.map(x => ({...x, done: ticked(x)})), "what happened")
// then: const reply = R.normalize(R.parseReply(job.output.output), "plan");
// R.check(reply, b)                               // {notes, warn}: dates the page did not find, lost deadlines, dropped asks, unanswered findings
// R.md("Council Tax Reduction", b, reply)         // the card and the page's findings as Markdown

Get a token for the steps below from /tokens.html.

Step 1 - a tiny client helper

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

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://next-thing.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":"next-thing"}' | 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. Save a worked-sample input above as input.json (task "plan" with the text, or "update" with the text and a progress string, each with the whole facts the page builds for it) to follow along.

# input.json: the run input object - {"task": "plan", "text": "Harrowmere District Council ...", "today": "2026-10-30", "minutes": "20", "progress": "", "facts": "Today: ...\nDates ...\nFindings ..."}
# - see the input table
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 next-thing:<task>:<inputHash>:a<attempt> - the lane is part of the key, so a plan and an update are two runs - and its one automatic reformat retry (sent with a retry_note, which the hash leaves out) is a new attempt number, a2, named in the page as one extra run. Any stable hash of the input works for your own key. Each run is metered on gpt-terra; the job reports charged_credits, and truncated is true when the available balance cut the reply short. The code below parses the JSON object out of the output and prints, for "update", the state line and the wins, then for both lanes the next action with its minutes, the summary, the numbered steps (with each step's status on an update), the deadlines, what is parked under later, the question and the last line.

KEY="next-thing:$(jq -r .task input.json):$(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 '(if .task == "update" then "STATE: \(.state)", (.wins[]? | "+ \(.)") else empty end),
  "NEXT (\(.next_action.minutes) min): \(.next_action.text)", .summary, "",
  (.steps | to_entries[]? | "  \(.key + 1). \(if .value.status then "[\(.value.status)] " else "" end)\(.value.action) - \(.value.minutes) min\(if (.value.by // "") != "" then ", by \(.value.by)" else "" end)"),
  "Deadlines:", (.deadlines[]? | "  \(.date) - \(.what)"),
  (.later[]? | "later: \(.item) - \(.why)"),
  (if (.question // "") != "" then "? \(.question)" else empty end), .last_line' reply.json

Step 6 - POST /run-stream

The same run as an SSE stream, with the same input.json and the same kind of key. Browsers receive progress ticks rather than text deltas; the final done message carries the job, whose output.output is the reply.

# SSE: tick/delta messages while the job runs, then a final message with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: next-thing:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json