← Plot Trellis / API
Tokens

Drive Plot Trellis from your own code

The structural critique the page runs is available over HTTP. A natural use is an outline you keep as a Markdown or OPML file in your writing folder: a script re-reads it after each writing session, asks for a critique, and later a re-critique that checks what you did with the last one.

Be clear about the split first: the model never sees your file, only the outline text you build. The caller turns the tree into indented lines with stable card ids (n0 is the story), computes the facts (scene balance per part, threads, prescan problems) and sends them along. The reply proposes operations on those ids; refusing the ones that name cards you do not have is the caller's job. The page's own tree, clipping and reconciler ship as plain scripts you can load in node - see the end of the examples.

Base URL and the envelope

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

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

The token is minted for this app (the guest call takes {"slug":"plot-trellis"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object: never wrap it as {"input": {…}} — that returns 200, warns unknown field 'input', and hides every field from the model.

Errors:

StatusCodeMeaning
400validation_errorThe body is not a JSON object.
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: critiques are metered and need a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

One system prompt serves two tasks. task picks the contract; anything else is read as critique and the reply's meta line says so.

taskWhenFindings carry
critiqueThe first structural read of an outline.No status.
recritiqueAfter you edited the outline - accepted or dismissed proposals, moved cards yourself. Needs previous_json.status: resolved / open for every earlier fid, new for new ones.

Every run body is a JSON object of string fields (the declared input schema; task, title, structure, outline and facts_json are required, and /estimate warns when one is missing):

FieldWhat it holds
task"critique" or "recritique".
titleThe working title (the root card's text).
structurethree-act, kishotenketsu, heros-journey, seven-point, mystery or free.
genre, loglineOptional; what the story is trying to be.
outlineThe tree as indented text, two spaces per level: <id> [kind] <text> -- note: <note>. Kinds: turn, setup, payoff, note; #words are threads. The page sends at most 220 cards: it caps depth first, then count, leaving a marker line such as (... 12 deeper cards under n40 not sent).
facts_jsonJSON string: cards, depth, scenes, balance (score and per-part scenes, share and expected share), prescan (codes missing_turn, empty_branch, dangling_setup, missing_setup, payoff_first, duplicate, imbalance with ids) and threads.
previous_jsonrecritique only. JSON string: the earlier findings and each proposed op with its state (accepted, dismissed, pending, stale, refused).
clip_noteWhat was not sent, in words; empty when nothing was cut.

The reply text is JSON Lines - one object per line, so a reply cut short still yields every whole line:

{"k":"meta","task":"critique","structure":"three-act","summary":"..."}
{"k":"finding","fid":"f1","kind":"missing_turn","branch":"n11","ids":["n11"],"text":"...","status":"open|resolved|new (recritique only)"}
{"k":"op","oid":"o1","op":"add","parent":"n21","index":1,"text":"...","kind":"turn","fixes":"f1","reason":"..."}
{"k":"op","oid":"o2","op":"move","id":"n18","parent":"n6","index":1,"reason":"..."}
{"k":"op","oid":"o3","op":"merge","ids":["n22","n13"],"text":"...","reason":"..."}
{"k":"op","oid":"o4","op":"rename","id":"n10","text":"...","reason":"..."}
{"k":"verdict","shape":"sound|workable|needs_work","strength":"...","priority":"..."}
{"k":"end"}

Finding kinds: missing_turn, dangling_setup, missing_setup, empty_branch, imbalance, duplicate, order, missing_beat, pacing, other. Zero findings and zero ops with shape: "sound" is a valid answer.

Worked examples

These are the bundled examples on the page: the exact run body the page builds and a reply produced against the shipped prompt (the page replays the same replies for free).

critique: a three-act novel with real gaps

Input:

{
 "task": "critique",
 "title": "Wren and the Winter Light",
 "structure": "three-act",
 "genre": "coming-of-age mystery novel",
 "logline": "When the light on Sable Point is automated, sixteen-year-old Wren has one winter to prove her father's logbooks hide a wreck the coast guard buried.",
 "outline": "(indented text - shown below)",
 "facts_json": "(a JSON string - decoded below)",
 "clip_note": ""
}

outline:

n0 Wren and the Winter Light
  n1 Act I - Setup
    n2 The last keeper
      n3 Wren rows the winter supplies out to the light
      n4 [setup] Her father locks the logbook drawer when she comes in #logbook
    n5 The letter
      n6 [turn] The coast guard writes: the light goes automatic in spring
      n7 Wren finds a page torn out of the 1987 log #logbook
    n8 Harbour town
      n9 Wren meets Ada, the harbour archivist
      n10 Ada refuses to open the 1987 files
  n11 Act II - Confrontation
    n12 The archive
      n13 Wren breaks into the archive at night
      n14 Ada catches her and, against her own rules, helps
    n15 The storm
      n16 A three-day storm cuts the island off
      n17 Her father tells half the story of the wreck -- note: He stops before the part about the second boat.
    n18 The diver
      n19 Wren spends her savings on a diver from the mainland
      n20 [setup] The diver mentions sonar showing a second hull #second-hull
    n21 Back to the archive
      n22 Wren breaks into the archive again at night
  n23 Act III - Resolution
    n24 The wreck
      n25 [turn] The dive brings up the ship's bell

facts_json, decoded:

{
 "cards": 26,
 "depth": 3,
 "scenes": 14,
 "balance": {
  "score": 82,
  "basis": "Three-act plot",
  "branches": [
   {
    "id": "n1",
    "scenes": 6,
    "words": 64,
    "share": 43,
    "expected": 25,
    "flag": "heavy"
   },
   {
    "id": "n11",
    "scenes": 7,
    "words": 81,
    "share": 50,
    "expected": 50,
    "flag": ""
   },
   {
    "id": "n23",
    "scenes": 1,
    "words": 12,
    "share": 7,
    "expected": 25,
    "flag": "thin"
   }
  ]
 },
 "prescan": [
  {
   "code": "missing_turn",
   "ids": [
    "n11"
   ],
   "branch": "n11"
  },
  {
   "code": "dangling_setup",
   "ids": [
    "n4"
   ],
   "branch": "n1"
  },
  {
   "code": "dangling_setup",
   "ids": [
    "n20"
   ],
   "branch": "n11"
  },
  {
   "code": "duplicate",
   "ids": [
    "n13",
    "n22"
   ],
   "branch": "n11"
  },
  {
   "code": "imbalance",
   "ids": [
    "n1"
   ],
   "branch": "n1"
  },
  {
   "code": "imbalance",
   "ids": [
    "n23"
   ],
   "branch": "n23"
  }
 ],
 "threads": [
  {
   "tag": "#logbook",
   "setups": [
    "n4"
   ],
   "payoffs": []
  },
  {
   "tag": "#second-hull",
   "setups": [
    "n20"
   ],
   "payoffs": []
  }
 ]
}

Reply (JSON Lines):

{"k": "meta", "task": "critique", "structure": "three-act", "summary": "A three-act shape fits this wreck mystery, but Act II only accumulates incidents and never turns, and Act III is one scene that cannot pay off the logbook or second-hull threads Act I and II plant. Trim Act I, give Act II a turn, let the ending breathe."}
{"k": "finding", "fid": "f1", "kind": "missing_turn", "branch": "n11", "ids": ["n11"], "text": "Act II (n11) has no card marked [turn]: the archive break-ins, the storm, and hiring the diver are incidents, not a point where Wren can no longer undo what she's done."}
{"k": "finding", "fid": "f2", "kind": "dangling_setup", "branch": "n1", "ids": ["n4"], "text": "The #logbook thread is only planted: n4's locked drawer gets no [payoff]. n7 finds a torn page but is untagged, so the drawer's secret is never shown being opened."}
{"k": "finding", "fid": "f3", "kind": "dangling_setup", "branch": "n11", "ids": ["n20"], "text": "The #second-hull thread set up at n20 (the diver's sonar finds a second hull) has no [payoff] card anywhere, including in Act III's single scene at n24."}
{"k": "finding", "fid": "f4", "kind": "duplicate", "branch": "n11", "ids": ["n13", "n22"], "text": "n13 and n22 are both 'Wren breaks into the archive at night' with nothing new at stake between them: same actor, same place, same risk, just repeated two chapters apart."}
{"k": "finding", "fid": "f5", "kind": "imbalance", "branch": "n1", "ids": ["n1"], "text": "Act I carries 43% of the outline's scenes against three-act's expected 25% (6 scenes vs. 1 for Act III) -- a long runway before the coast-guard letter forces the clock to start."}
{"k": "finding", "fid": "f6", "kind": "imbalance", "branch": "n23", "ids": ["n23"], "text": "Act III is one scene (7% vs. an expected 25%): the dive brings up the bell, but neither dangling thread (#logbook, #second-hull) nor the father's half-told story (n17) gets room to land."}
{"k": "op", "oid": "o1", "op": "add", "parent": "n18", "index": 2, "kind": "turn", "text": "Spending her last savings on the diver, Wren passes the point of no return", "fixes": "f1", "reason": "Marks the one choice in Act II that can't be undone, giving the act the turn three-act structure expects."}
{"k": "op", "oid": "o2", "op": "add", "parent": "n24", "index": 1, "kind": "payoff", "text": "Her father finishes the wreck story he stopped mid-storm, naming what the second hull is #second-hull", "fixes": "f3", "reason": "Pays off the sonar setup at n20 and finally finishes the story n17 left half-told, inside the resolution act."}
{"k": "op", "oid": "o3", "op": "add", "parent": "n24", "index": 2, "kind": "payoff", "text": "Wren finally unlocks the logbook drawer and reads the torn page for herself #logbook", "fixes": "f2", "reason": "Pays off the drawer setup at n4, which the outline otherwise never resolves."}
{"k": "op", "oid": "o4", "op": "add", "parent": "n24", "index": 3, "kind": "note", "text": "Wren decides what to tell the coast guard before the light goes automatic", "fixes": "f6", "reason": "Gives the resolution a closing beat of its own instead of ending on the bell alone, growing Act III's share."}
{"k": "op", "oid": "o5", "op": "merge", "ids": ["n9", "n10"], "text": "Wren meets Ada, the harbour archivist, who refuses to open the 1987 files", "fixes": "f5", "reason": "Combines one conversation's two beats into one scene, trimming Act I's scene count toward its expected share."}
{"k": "op", "oid": "o6", "op": "merge", "ids": ["n12", "n21"], "text": "The archive", "fixes": "f4", "reason": "Puts both archive break-ins under one chapter so n13 and n22 sit side by side as one beat instead of two separate chapter-level trips."}
{"k": "verdict", "shape": "needs_work", "strength": "The through-line is tight: a locked drawer escalates cleanly to a torn page, a defiant archivist, a hired diver and a second hull, all before the dive.", "priority": "First give Act II a turn (hiring the diver) and grow Act III enough to pay off the logbook and second-hull threads before the story ends."}
{"k": "end"}

recritique: the same novel after every proposal was accepted

Input (previous_json carries the first critique and what became of each proposal):

{
 "task": "recritique",
 "title": "Wren and the Winter Light",
 "structure": "three-act",
 "genre": "coming-of-age mystery novel",
 "logline": "When the light on Sable Point is automated, sixteen-year-old Wren has one winter to prove her father's logbooks hide a wreck the coast guard buried.",
 "outline": "(indented text - shown below)",
 "facts_json": "(a JSON string - decoded below)",
 "clip_note": "",
 "previous_json": "(a JSON string - decoded below)"
}

outline:

n0 Wren and the Winter Light
  n1 Act I - Setup
    n2 The last keeper
      n3 Wren rows the winter supplies out to the light
      n4 [setup] Her father locks the logbook drawer when she comes in #logbook
    n5 The letter
      n6 [turn] The coast guard writes: the light goes automatic in spring
      n7 Wren finds a page torn out of the 1987 log #logbook
    n8 Harbour town
      n9 Wren meets Ada, the harbour archivist, who refuses to open the 1987 files -- note: Merged from "Ada refuses to open the 1987 files"
  n11 Act II - Confrontation
    n12 The archive -- note: Merged from "Back to the archive"
      n13 Wren breaks into the archive at night
      n14 Ada catches her and, against her own rules, helps
      n22 Wren breaks into the archive again at night
    n15 The storm
      n16 A three-day storm cuts the island off
      n17 Her father tells half the story of the wreck -- note: He stops before the part about the second boat.
    n18 The diver
      n19 Wren spends her savings on a diver from the mainland
      n20 [setup] The diver mentions sonar showing a second hull #second-hull
      n26 [turn] Spending her last savings on the diver, Wren passes the point of no return
  n23 Act III - Resolution
    n24 The wreck
      n25 [turn] The dive brings up the ship's bell
      n27 [payoff] Her father finishes the wreck story he stopped mid-storm, naming what the second hull is #second-hull
      n28 [payoff] Wren finally unlocks the logbook drawer and reads the torn page for herself #logbook
      n29 [note] Wren decides what to tell the coast guard before the light goes automatic

facts_json, decoded:

{
 "cards": 28,
 "depth": 3,
 "scenes": 17,
 "balance": {
  "score": 96,
  "basis": "Three-act plot",
  "branches": [
   {
    "id": "n1",
    "scenes": 5,
    "words": 73,
    "share": 29,
    "expected": 25,
    "flag": ""
   },
   {
    "id": "n11",
    "scenes": 8,
    "words": 97,
    "share": 47,
    "expected": 50,
    "flag": ""
   },
   {
    "id": "n23",
    "scenes": 4,
    "words": 55,
    "share": 24,
    "expected": 25,
    "flag": ""
   }
  ]
 },
 "prescan": [
  {
   "code": "duplicate",
   "ids": [
    "n13",
    "n22"
   ],
   "branch": "n11"
  }
 ],
 "threads": [
  {
   "tag": "#logbook",
   "setups": [
    "n4"
   ],
   "payoffs": [
    "n28"
   ]
  },
  {
   "tag": "#second-hull",
   "setups": [
    "n20"
   ],
   "payoffs": [
    "n27"
   ]
  }
 ]
}

previous_json, decoded:

{
 "findings": [
  {
   "fid": "f1",
   "kind": "missing_turn",
   "branch": "n11",
   "ids": [
    "n11"
   ],
   "text": "Act II (n11) has no card marked [turn]: the archive break-ins, the storm, and hiring the diver are incidents, not a point where Wren can no longer undo what she's done."
  },
  {
   "fid": "f2",
   "kind": "dangling_setup",
   "branch": "n1",
   "ids": [
    "n4"
   ],
   "text": "The #logbook thread is only planted: n4's locked drawer gets no [payoff]. n7 finds a torn page but is untagged, so the drawer's secret is never shown being opened."
  },
  {
   "fid": "f3",
   "kind": "dangling_setup",
   "branch": "n11",
   "ids": [
    "n20"
   ],
   "text": "The #second-hull thread set up at n20 (the diver's sonar finds a second hull) has no [payoff] card anywhere, including in Act III's single scene at n24."
  },
  {
   "fid": "f4",
   "kind": "duplicate",
   "branch": "n11",
   "ids": [
    "n13",
    "n22"
   ],
   "text": "n13 and n22 are both 'Wren breaks into the archive at night' with nothing new at stake between them: same actor, same place, same risk, just repeated two chapters apart."
  },
  {
   "fid": "f5",
   "kind": "imbalance",
   "branch": "n1",
   "ids": [
    "n1"
   ],
   "text": "Act I carries 43% of the outline's scenes against three-act's expected 25% (6 scenes vs. 1 for Act III) -- a long runway before the coast-guard letter forces the clock to start."
  },
  {
   "fid": "f6",
   "kind": "imbalance",
   "branch": "n23",
   "ids": [
    "n23"
   ],
   "text": "Act III is one scene (7% vs. an expected 25%): the dive brings up the bell, but neither dangling thread (#logbook, #second-hull) nor the father's half-told story (n17) gets room to land."
  }
 ],
 "ops": [
  {
   "oid": "o1",
   "op": "add",
   "state": "accepted",
   "parent": "n18",
   "text": "Spending her last savings on the diver, Wren passes the point of no return"
  },
  {
   "oid": "o2",
   "op": "add",
   "state": "accepted",
   "parent": "n24",
   "text": "Her father finishes the wreck story he stopped mid-storm, naming what the second hull is #second-hull"
  },
  {
   "oid": "o3",
   "op": "add",
   "state": "accepted",
   "parent": "n24",
   "text": "Wren finally unlocks the logbook drawer and reads the torn page for herself #logbook"
  },
  {
   "oid": "o4",
   "op": "add",
   "state": "accepted",
   "parent": "n24",
   "text": "Wren decides what to tell the coast guard before the light goes automatic"
  },
  {
   "oid": "o5",
   "op": "merge",
   
... (shortened here)

Reply (JSON Lines):

{"k": "meta", "task": "recritique", "structure": "three-act", "summary": "Each act now turns on its own card, and both open threads close in Act III, which also carries its fair share of scenes. One carryover remains: merging the two archive chapters left a duplicated break-in scene sitting inside the same chapter."}
{"k": "finding", "fid": "f1", "kind": "missing_turn", "branch": "n11", "ids": ["n11", "n26"], "text": "Resolved: n26 now marks Act II's own turn, \"Spending her last savings on the diver, Wren passes the point of no return,\" placed at the end of n18 where the act hands off to Act III.", "status": "resolved"}
{"k": "finding", "fid": "f2", "kind": "dangling_setup", "branch": "n1", "ids": ["n4", "n28"], "text": "Resolved: the #logbook thread set up at n4 now pays off at n28, \"Wren finally unlocks the logbook drawer and reads the torn page for herself,\" in Act III's n24.", "status": "resolved"}
{"k": "finding", "fid": "f3", "kind": "dangling_setup", "branch": "n11", "ids": ["n20", "n27"], "text": "Resolved: the #second-hull thread set up at n20 (the diver's sonar) now pays off at n27, where the father names what the second hull is, inside Act III's n24.", "status": "resolved"}
{"k": "finding", "fid": "f4", "kind": "duplicate", "branch": "n11", "ids": ["n13", "n22"], "text": "Still open: n13 and n22 are both \"Wren breaks into the archive at night.\" Merging the chapters put them side by side in n12, right across n14 where Ada already agrees to help - the second break-in now reads as leftover, not escalation.", "status": "open"}
{"k": "finding", "fid": "f5", "kind": "imbalance", "branch": "n1", "ids": ["n1"], "text": "Resolved: Act I now holds 29% of scenes against an expected 25% (facts_json balance, no flag), down from 43%, after the harbour-town merge trimmed a redundant scene.", "status": "resolved"}
{"k": "finding", "fid": "f6", "kind": "imbalance", "branch": "n23", "ids": ["n23"], "text": "Resolved: Act III now holds 24% of scenes against an expected 25% (no flag in facts_json), up from 7%, once the father's full wreck story, the logbook payoff, and Wren's decision were added under n24.", "status": "resolved"}
{"k": "op", "oid": "o1", "op": "merge", "ids": ["n13", "n22"], "text": "Wren breaks into the archive at night", "fixes": "f4", "reason": "Collapses the repeated break-in into one scene now that both sit in the merged n12; n14's help already covers why a second break-in isn't needed."}
{"k": "verdict", "shape": "workable", "strength": "All three acts now turn on a card of their own, both dangling threads close inside Act III, and scene balance across acts sits within a few points of the template's expected share.", "priority": "Merge n13 and n22: the archive-chapter merge left two near-identical break-in scenes sitting in the same chapter, right next to the scene where Ada already agrees to help."}
{"k": "end"}

critique: a mystery whose clue is paid off before it is planted

Input:

{
 "task": "critique",
 "title": "The Cider Press Will",
 "structure": "mystery",
 "genre": "village cozy mystery",
 "logline": "A retired schoolteacher investigates the death of the cider-press owner whose will was changed the week he died.",
 "outline": "(indented text - shown below)",
 "facts_json": "(a JSON string - decoded below)",
 "clip_note": ""
}

outline:

n0 The Cider Press Will
  n1 The crime
    n2 Harvest week
      n3 [turn] The press owner is found dead in the cider barn
      n4 Four heirs arrive for the reading of the will
  n5 The investigation
    n6 First questions
      n7 Margaret interviews the eldest son in the orchard
      n8 [payoff] The ledger shows the will was witnessed on a day the solicitor was abroad #ledger
    n9 The heirs
      n10 Margaret interviews the eldest son again at the pub
      n11 The niece's alibi holds
      n12 The housekeeper lies about the barn key
    n13 The second will
      n14 [turn] A second will turns up in the church
      n15 The solicitor's clerk goes missing
  n16 The reveal
    n17 The tasting
      n18 [setup] Margaret notices the press ledger has a page in different ink #ledger
      n19 [turn] Margaret names the clerk at the harvest supper

facts_json, decoded:

{
 "cards": 20,
 "depth": 3,
 "scenes": 11,
 "balance": {
  "score": 96,
  "basis": "Mystery",
  "branches": [
   {
    "id": "n1",
    "scenes": 2,
    "words": 23,
    "share": 18,
    "expected": 20,
    "flag": ""
   },
   {
    "id": "n5",
    "scenes": 7,
    "words": 65,
    "share": 64,
    "expected": 60,
    "flag": ""
   },
   {
    "id": "n16",
    "scenes": 2,
    "words": 24,
    "share": 18,
    "expected": 20,
    "flag": ""
   }
  ]
 },
 "prescan": [
  {
   "code": "payoff_first",
   "ids": [
    "n8",
    "n18"
   ],
   "branch": "n5"
  }
 ],
 "threads": [
  {
   "tag": "#ledger",
   "setups": [
    "n18"
   ],
   "payoffs": [
    "n8"
   ]
  }
 ]
}

Reply (JSON Lines):

{"k":"meta","task":"critique","structure":"mystery","summary":"Each act turns on its own beat (the body, the second will, naming the clerk), which suits a mystery. The investigation act fairly carries the bulk of the scene count. The one snag: the ledger's setup clue lands after the payoff it explains."}
{"k":"finding","fid":"f1","kind":"order","branch":"n16","ids":["n8","n18"],"text":"n8, the reveal that the will was witnessed while the solicitor was abroad, lands in First Questions early in the investigation, but n18, Margaret noticing the ledger's mismatched ink that would uncover this, doesn't happen until The Tasting near the end - the payoff precedes its setup."}
{"k":"op","oid":"o1","op":"move","id":"n18","parent":"n6","index":1,"fixes":"f1","reason":"Moves the ink-mismatch discovery before the ledger reveal (n8) so Margaret finds the clue before it's explained, restoring cause before effect."}
{"k":"verdict","shape":"workable","strength":"Each act lands its own turn (n3's death, n14's second will, n19's naming of the clerk), and the second interview with the eldest son escalates once the backdated will surfaces.","priority":"Move n18 (the ledger's mismatched ink) ahead of n8 (what it reveals) so the clue is found before it's explained."}
{"k":"end"}

critique: a kishotenketsu story that is already sound

Input:

{
 "task": "critique",
 "title": "The Teahouse at Kinosaki",
 "structure": "kishotenketsu",
 "genre": "literary short story",
 "logline": "An old tea master's last season, told through the guests who come for the onsen and stay for the tea.",
 "outline": "(indented text - shown below)",
 "facts_json": "(a JSON string - decoded below)",
 "clip_note": ""
}

outline:

n0 The Teahouse at Kinosaki
  n1 Ki - Introduction
    n2 Mrs Ota opens the shutters before the first bathers
    n3 She sets out the cracked cup she never sells #cup
  n4 Sho - Development
    n5 A student comes every evening and orders the cheapest tea
    n6 Mrs Ota starts giving him the better leaves without a word
  n7 Ten - Twist
    n8 [turn] A crane lands in the river and the whole street stops to watch
    n9 [setup] The student sketches the crane on a napkin and leaves it in the cracked cup #cup
  n10 Ketsu - Reconciliation
    n11 Winter: the student does not come back
    n12 [payoff] Mrs Ota frames the napkin above the counter, the cup beneath it #cup

facts_json, decoded:

{
 "cards": 13,
 "depth": 2,
 "scenes": 8,
 "balance": {
  "score": 100,
  "basis": "Kishotenketsu",
  "branches": [
   {
    "id": "n1",
    "scenes": 2,
    "words": 21,
    "share": 25,
    "expected": 25,
    "flag": ""
   },
   {
    "id": "n4",
    "scenes": 2,
    "words": 23,
    "share": 25,
    "expected": 25,
    "flag": ""
   },
   {
    "id": "n7",
    "scenes": 2,
    "words": 31,
    "share": 25,
    "expected": 25,
    "flag": ""
   },
   {
    "id": "n10",
    "scenes": 2,
    "words": 22,
    "share": 25,
    "expected": 25,
    "flag": ""
   }
  ]
 },
 "prescan": [],
 "threads": [
  {
   "tag": "#cup",
   "setups": [
    "n9"
   ],
   "payoffs": [
    "n12"
   ]
  }
 ]
}

Reply (JSON Lines):

{"k":"meta","task":"critique","structure":"kishotenketsu","summary":"Ki, Sho, Ten and Ketsu each carry two scenes with an even 25% share, and only Ten lands a marked turn (the crane stopping the street), which is all this form requires. The #cup thread set up in Ten resolves cleanly in Ketsu, giving the plotless structure a real spine."}
{"k":"verdict","shape":"sound","strength":"Each part is built from exactly two scenes, the single required turn sits in Ten, and the napkin-in-the-cup image threads from Ten setup to Ketsu payoff without forcing a conflict the form does not need.","priority":"The logline promises guests (plural) across a last season; only one guest's thread is drafted. Watch that Mrs Ota's season still reads as full, not just this one student's arc."}
{"k":"end"}

The page builds the outline text and checks the reply; the API does not

Over the API you get the critique as JSON Lines. Building outline and facts_json, and refusing proposals that name cards you do not have, is the caller's job. The page's own code is plain scripts you can load in node after global.window = global: tree.js (Trellis.buildInput(task, doc) builds this exact run body, clipping by depth and count with marker lines; Trellis.prescan(doc) and Trellis.balance(doc) are the facts), critique.js (TrellisCritique.parse(text), .reconcile(parsed, doc, input), .applyOp(doc, op)) and io.js (Markdown and OPML in and out).

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://plot-trellis.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":"plot-trellis"}' | 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 (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.

# input.json: the run input object - {"task": "critique", "title": "...", "structure": "three-act", "outline": "n0 ...", "facts_json": "{...}"}
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 plot-trellis:critique:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.

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

Step 6 — POST /run-stream

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

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