Run Walk Away from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads the situation and the mandate (the Mandascan per axis checked for gaps and order, a precise anchor, a concession ladder that shrinks 50/30/20 percent of the gap, the Ackerman steps for a buyer's price, the zone of agreement when their limit is known), reads what the other side said, and marks your escalation points and walk-aways private, then hands all of it to the run as facts. The run writes the plan or the reply; the page then holds it to the skill's discipline (negreply.js): questions that start with What or How, labels that name the emotion, every concession traded, nothing split, no private figure in a line meant for the other side, no figure the input does not give.
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":"walk-away"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. 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 key (task, situation, mandate, facts) is missing. The page always sends all five of task, situation, mandate, said, facts; said is an empty string for prepare. |
| 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: both lanes are metered and need 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 comes first
Two lanes, chosen by task, in the order a negotiation runs - before the room, then in it:
prepare- the plan before the room: the mandate gaps and how to close them, their position, objective and stake per axis, the anchor line, a ladder of three or four concessions each paid for by a counter-ask, five or six calibrated questions, three accusation-audit labels, the shared common objective, the tactical pause, a pre-mortem and the back-brief.respond- the reply to what they just said: the triage of the no (emotional, belief, bad-faith, identity, tactical), the root objection, what is said underneath, the label, the question and the line, where to go silent, the trade if any, what to avoid and where it leaves you against your mandate.
A missing or unknown task is answered as prepare. Both lanes send the same five fields. The facts are the page's worked-out lines and a line starting "must:" is a rule the reply has to hold. When nothing has been said yet, the page rehearses the plan's pre-mortem objection as said.
The input
| Field | Required | What |
|---|---|---|
task | yes | prepare or respond. If it is missing or neither, do prepare and set "task": "prepare". |
situation | yes | the domain, the stage, the situation, the counterparty, their words verbatim, the authority limit, the BATNA, their likely alternative and any lines already planned; "not given" means not given. |
mandate | yes | one line per axis: what more or less is better, and the five points as entered. |
said | send it (empty unless task is respond) | respond task only: what the other side just said. |
facts | yes | the page's worked-out lines for this task. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
From the source skill, @samber/influence-and-negotiation - the only standard the run applies:
- Seventy percent of the outcome is set before the room: the mandate, the map of who decides and the walk-away are written down first. Improvising on a fuzzy mandate over-asks early and over-concedes late.
- Negotiate the underlying stake, not the position. Position is what they say, objective is what they optimise for, stake is what is personally at risk; solving for the stake produces movement.
- The party more emotionally invested loses. Count to four after every offer or label; let silence work.
- The BATNA sizes the walk-away in preparation and is then put away: in the room, behave as if this is the only deal.
- Anchor first when the anchor is defensible; a precise number reads as calculated. For a salary ask, a bolstering range whose bottom is the real target.
- Trade, never give: every concession is paired with a counter-ask - "I might be able to move on X if you can help me with Y". Never split the difference. Concessions shrink as you approach your objective.
- Calibrated questions start with what or how, never why, and are never yes or no. Labels name the emotion ("It sounds like...", "It seems like...") and are followed by silence. An accusation audit names their likely objections before they do.
- Triage every no before answering it: emotional (name the emotion first, facts after), belief-based (do not refute the belief, find the stake behind it), bad-faith (drop the ego, state the facts cleanly, open a face-saving exit), identity-protective (hand back a controllable part of the form), tactical (name the pattern, trade, never give). The four root commercial objections are price, timing, authority and no need.
- Real deadlines have specific consequences; ask "what happens if we miss that date?" before accepting time pressure.
- Number discipline: your limits - the escalation point and the walk-away - never appear in anything said or written to the other side.
- Influence preserves the other side's free will; manipulation strips it. Never script a lie, a fake deadline or a bluff.
House rules
- Use only the input. Never invent a fact about the counterparty, a competitor's price, a market figure, a deadline or a past event. Every figure about the deal, the parties or the market appears in the input, except the precise variations the facts give; procedural suggestions (a short break, counting to four) are not facts.
- A stake you infer is a hypothesis: say so ("possibly", "test this").
- Never put a private figure from the facts in a line meant for the other side - a question, a label, an opening line, a script or a reply.
- Write in the language of the situation; keep the JSON keys in English.
- Refer to people by name or role; never assume a gender.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
prepare
Prepare the negotiation before the room.
summary- one sentence, at most 40 words: how ready the mandate is and the one thing to settle first.gaps- one short string per mandate gap the facts list, in that order, each saying how to close it;[]when none.pos- one object per axis in the mandate, in that order:axis(exactly as written),position(their stated or likely position),objective(what they are optimising for),stake(the likely personal stake - a hypothesis to test).anchor-axis(the axis to anchor first),line(the sentence that states your opening, using the entry or the facts' precise anchor, with no private figure; for a salary ask it may be a bolstering range whose bottom is that figure),silence(one short sentence on holding the silence after it).ladder- three or four objects, the concessions in order:axis,give(the move, using the facts' ladder figures where the axis has them),get(the counter-ask that pays for it). On each axis, each give is smaller than the one before.questions- five or six calibrated questions, each starting with What or How.labels- three accusation-audit labels, each starting with "It seems", "It sounds", "It looks" or "It feels".sco- one sentence: the shared common objective both sides can say yes to.pause-triggers(two or three signals to call a break),script(one or two sentences to call it).premortem-best(objection- the one most likely to hit, as they would say it;label;reframe),weakest(one sentence: the objection not worth preparing for, and why),surprise(move- the gambit you might not see coming;redirect).back_brief- one string per axis: what they must restate in their own words before any agreement.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "prepare",
"summary": "one sentence",
"gaps": [ "a gap and how to close it" ],
"pos": [ { "axis": "Price", "position": "their position", "objective": "their objective", "stake": "possibly ... - test this" } ],
"anchor": { "axis": "Price", "line": "the opening sentence", "silence": "one sentence" },
"ladder": [ { "axis": "Price", "give": "the move", "get": "the counter-ask" } ],
"questions": [ "What ...?" ],
"labels": [ "It sounds like ..." ],
"sco": "one sentence",
"pause": { "triggers": [ "a signal" ], "script": "one or two sentences" },
"premortem": { "best": { "objection": "as they would say it", "label": "It sounds like ...", "reframe": "one sentence" }, "weakest": "one sentence", "surprise": { "move": "the gambit", "redirect": "one sentence" } },
"back_brief": [ "one string per axis" ],
"caveats": [ ]
}
The page holds it to the skill's discipline and its own facts (check() in negreply.js; any warning makes the reply flagged): the reply answers the task it was sent; nothing splits the difference; no figure the input does not give; at least as many gaps as the page found; one position-objective-stake row per axis, in order, each with a stake; an anchor on a mandate axis whose line states the entry or the precise anchor; three or four concessions, each with a move and a counter-ask, on mandate axes; five or six questions starting with What or How and never why; three labels starting It seems, It sounds, It looks or It feels; a shared objective; a pause with two or three triggers and a script; a complete pre-mortem; one back-brief line per axis; and no private figure in the anchor line, the ladder, the questions, the labels, the shared objective, the pause script, the pre-mortem's label, reframe and redirect, or the back-brief.
respond
Coach the reply to what the other side just said.
summary- one sentence, at most 40 words: what is really going on and the move.triage- exactly one of: emotional, belief, bad-faith, identity, tactical.root- exactly one of: price, timing, authority, no-need, none.read- one or two sentences: what is being said underneath, and the likely stake - a hypothesis.reply- what to say next, in order:label(names what you heard, starting "It sounds", "It seems", "It looks" or "It feels"),question(one calibrated question starting with What or How),line(one or two sentences that hold the mandate - for bad faith, the facts stated cleanly and a face-saving exit; for tactical, the pattern named).silence- one sentence: where to stop talking and count to four.trade- when a concession is the right move:giveandget(the counter-ask); otherwise both "".avoid- two or three short strings: the replies that would lose ground here.mandate_check- one sentence: where this reply leaves you against your objective and your escalation point, without stating either figure.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "respond",
"summary": "one sentence",
"triage": "bad-faith",
"root": "price",
"read": "one or two sentences",
"reply": { "label": "It sounds like ...", "question": "What ...?", "line": "one or two sentences" },
"silence": "one sentence",
"trade": { "give": "", "get": "" },
"avoid": [ "a reply that loses ground" ],
"mandate_check": "one sentence",
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; nothing splits the difference; no figure the input does not give; a triage and a root from the allowed lists; a label starting It seems, It sounds, It looks or It feels; a question starting with What or How; a line; a trade that never gives without a counter-ask; two or three things to avoid; and no private figure in the reply, the trade or the mandate check. Notes that do not flag: a root that differs from the page's own read, and a bad-faith line with no sign of a face-saving exit.
Worked examples
Three runs from the page's three free samples (fictional companies, people and figures): the plan for the vendor in an enterprise deal, the reply to that deal's bad-faith price claim, and the hiring manager's rehearsed reply to the objection the plan predicted. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the situation and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.
prepare - A vendor in an enterprise deal
The page read it as B2B sales, 6 of 6 intake answers given, mandate: close the mandate gaps; axes: Price, Term, Payment terms, Implementation services; gaps: Implementation services: no entry, ideal, objective, escalation, rupture - derive the rupture from your BATNA. Private figures: $180k (escalation, Price), $160k (rupture, Price), 12 months (rupture, Term), 60 days (escalation, Payment terms), 90 days (rupture, Payment terms).
Input (the situation is 1096 characters; the first 885 are shown):
{
"task": "prepare",
"situation": "Domain: B2B sales\nStage: Mid-negotiation\nThe situation: We sell Corvane, a fleet-maintenance platform, to Halvorine Freight, a 1,400-truck carrier. A 200k ACV deal in its third month; procurement is running the RFP and the CFO signs. Losing it misses our quarter; winning it is our largest logistics logo.\nCounterparty: Dana Okarvo, procurement lead (in the room); Priya Shandrel, head of maintenance and our champion; the CFO, off-stage, signs\nWhat they have said, verbatim:\n- \"Your competitor quoted us half that.\"\n- \"We need a decision by the 28th, our budget closes.\"\n- \"Maintenance loves the pilot results.\"\nAuthority - what can be committed without checking: Up to 10% discount and payment terms to 45 days without asking anyone\nBATNA - the next best option if this fails: Two other carriers in late stage this quarter, smaller deals\nTheir likely alternative: Renew their current\n\n[... 211 more characters cut from this display ...]",
"mandate": "Price (more is better): entry $240k; ideal $220k; objective $200k; escalation $180k; rupture $160k; their stated position $150k; their estimated limit $175k\nTerm (more is better): entry 36 months; ideal 36 months; objective 24 months; escalation 24 months; rupture 12 months\nPayment terms (less is better): entry 30 days; ideal 30 days; objective 45 days; escalation 60 days; rupture 90 days\nImplementation services (more is better): entry not set; ideal not set; objective not set; escalation not set; rupture not set",
"said": "",
"facts": "Intake: 6 of 6 answers given.\nMandate gaps the page found: Implementation services: no entry, ideal, objective, escalation, rupture - derive the rupture from your BATNA.\nPrice: concession ladder from the entry to the objective, shrinking 50/30/20 percent of the gap: $220k, then $208k, then $200k; a precise anchor instead of the round entry: $238.4k; zone of agreement $160k to $175k.\nTerm: concession ladder from the entry to the objective, shrinking 50/30/20 percent of the gap: 30 months, then 26 months, then 24 months.\nPayment terms: concession ladder from the entry to the objective, shrinking 50/30/20 percent of the gap: 38 days, then 42 days, then 45 days.\nPrivate figures - never in a line said to the other side: $180k (escalation, Price), $160k (rupture, Price), 12 months (rupture, Term), 60 days (escalation, Payment terms), 90 days (rupture, Payment terms).\nmust: every question starts with What or How - never Why, never yes or no.\nmust: every concession on the ladder carries a counter-ask (I might be able to move on X if you can help me with Y); never split the difference.\nWhat the page reads in their words: \"Your competitor quoted us half that.\" - price; a competing alternative named | \"We need a decision by the 28th, our budget closes.\" - price; time or ultimatum pressure | \"Maintenance loves the pilot results.\" - no root objection."
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "prepare",
"summary": "Price, term and payment terms are set, but Implementation services has no points at all and the estimated limit sits below your objective; settle the implementation mandate before the next session.",
"gaps": [
"Implementation services: write all five points before the room - start the rupture from the BATNA (the two smaller late-stage carriers), so you know what scope you would give away free rather than lose the deal, then set the objective and [... 16 more characters cut from this display]"
],
"pos": [
{
"axis": "Price",
"position": "Half of the quote - they cite a competitor and a stated position of $150k.",
"objective": "A total cost the CFO can defend against renewing the spreadsheet-and-contractor setup before the budget closes on the 28th.",
"stake": "Possibly Dana Okarvo is measured on savings against the first quote, so a visible reduction matters more than the final figure - test this."
},
{
"axis": "Term",
"position": "Likely a shorter commitment than 36 months while the platform is new to them.",
"objective": "Flexibility to exit if the rollout to 1,400 trucks underdelivers.",
"stake": "Possibly the CFO does not want to own a multi-year commitment that fails in public - test this."
},
{
"axis": "Payment terms",
"position": "Likely longer than 30 days, to ease the budget close.",
"objective": "Cash timing that fits a budget closing on the 28th.",
"stake": "Possibly procurement wants a concession it can show without touching price - test this."
},
{
"axis": "Implementation services",
"position": "Likely expects rollout help included, given the pilot.",
"objective": "A rollout that does not land on the maintenance team's shoulders.",
"stake": "Possibly Priya Shandrel is personally exposed if the rollout stalls after championing the pilot - test this."
}
],
"anchor": {
"axis": "Price",
"line": "For the full fleet of 1,400 trucks on a 36-month term, with the scope Priya's team piloted, our proposal is $238.4k a year.",
"silence": "Stop after the figure and count to four in silence, even if Dana Okarvo reacts to it."
},
"ladder": [
{
"axis": "Price",
"give": "Move from $238.4k to $220k.",
"get": "I might be able to move to $220k if you can help me with a 36-month term signed before the 28th."
},
{
"axis": "Price",
"give": "Move to $208k.",
"get": "I might be able to move to $208k if you can help me with Halvorine Freight as a named reference and a case study on the pilot results."
},
{
"axis": "Payment terms",
"give": "Move from 30 to 38 days.",
"get": "I might be able to move to 38 days if you can help me with a meeting with the CFO before the 28th to confirm the signature path."
},
{
"axis": "Price",
"give": "Move to $200k as the last step, and say it is the last.",
"get": "I might be able to move to $200k if you can help me with a term of 30 months or more and the order signed by the 28th."
}
],
"questions": [
"What happens on your side if the decision slips past the 28th?",
"What does the competing quote include, and how does its scope compare with what the pilot covered?",
"How will the CFO compare this against renewing the spreadsheet-and-contractor setup for another year?",
"What would success look like for your maintenance team this year?",
"How would Priya's team measure the pilot results once the platform runs across all 1,400 trucks?",
"What would the CFO need to see to sign before the budget closes?"
],
"labels": [
"It seems like our number landed well above what you had budgeted.",
"It sounds like you may feel we are asking for a premium on something you can get elsewhere.",
"It feels like you are caught between a budget that closes on the 28th and a maintenance team that wants this."
],
"sco": "A fleet-maintenance rollout across all 1,400 trucks that Priya's team can run and the CFO can defend at the budget close.",
"pause": {
"triggers": [
"A new figure or a new decision-maker appears that you have not prepared for.",
"You feel the urge to answer the competitor claim with a price move on the spot.",
"The conversation turns to splitting a gap rather than trading."
],
"script": "This deserves a proper answer rather than a quick one. Let me take a short break and come back to you with something we can both work with."
},
"premortem": {
"best": {
"objection": "Your competitor quoted us half that - we cannot justify $240k to the CFO.",
"label": "It sounds like you need a number you can defend to the CFO, and ours makes that hard.",
"reframe": "Rather than argue the other quote, put the scope side by side with the pilot results and ask what a like-for-like comparison would show."
},
"weakest": "No need is not worth preparing for: maintenance loves the pilot results and their alternative is another year of spreadsheets and contractors.",
"surprise": {
"move": "After a verbal yes, the CFO comes back through Dana Okarvo asking for one more cut as the price of signing by the 28th.",
"redirect": "Name the pattern calmly, then ask what they would move in return, since the agreed terms were built as a package."
}
},
"back_brief": [
"Price: the annual figure agreed, and what it covers.",
"Term: the length of the commitment and when it starts.",
"Payment terms: the number of days and from which invoice.",
"Implementation services: what rollout help is included, and what is out of scope."
],
"caveats": [
"Drop the planned line asking why their budget is so low - it starts with why and puts Dana Okarvo on the defensive.",
"Drop the planned line about splitting the difference - trade each move for something instead.",
"The competitor figure is their claim; do not guess its real scope or price, ask for it.",
"Their estimated limit sits below your objective - if it holds, the deal only closes beneath it, so test the limit before conceding."
]
}
Cut from this display only (the saved reply and the check use the full text): gaps[0] (254 characters, 238 shown).
The card the page exports for the room (R.card) - no private figure on it:
IN THE ROOM
Open: For the full fleet of 1,400 trucks on a 36-month term, with the scope Priya's team piloted, our proposal is $238.4k a year.
Then: Stop after the figure and count to four in silence, even if Dana Okarvo reacts to it.
Shared objective: A fleet-maintenance rollout across all 1,400 trucks that Priya's team can run and the CFO can defend at the budget close.
Accusation audit:
- It seems like our number landed well above what you had budgeted.
- It sounds like you may feel we are asking for a premium on something you can get elsewhere.
- It feels like you are caught between a budget that closes on the 28th and a maintenance team that wants this.
Questions:
- What happens on your side if the decision slips past the 28th?
- What does the competing quote include, and how does its scope compare with what the pilot covered?
- How will the CFO compare this against renewing the spreadsheet-and-contractor setup for another year?
- What would success look like for your maintenance team this year?
- How would Priya's team measure the pilot results once the platform runs across all 1,400 trucks?
- What would the CFO need to see to sign before the budget closes?
If they say: Your competitor quoted us half that - we cannot justify $240k to the CFO.
It sounds like you need a number you can defend to the CFO, and ours makes that hard.
Rather than argue the other quote, put the scope side by side with the pilot results and ask what a like-for-like comparison would show.
If they try: After a verbal yes, the CFO comes back through Dana Okarvo asking for one more cut as the price of signing by the 28th.
Name the pattern calmly, then ask what they would move in return, since the agreed terms were built as a package.
Break: A new figure or a new decision-maker appears that you have not prepared for.; You feel the urge to answer the competitor claim with a price move on the spot.; The conversation turns to splitting a gap rather than trading. -> This deserves a proper answer rather than a quick one. Let me take a short break and come back to you with something we can both work with.
Before any yes, they restate:
- Price: the annual figure agreed, and what it covers.
- Term: the length of the commitment and when it starts.
- Payment terms: the number of days and from which invoice.
- Implementation services: what rollout help is included, and what is out of scope.
What step 5 prints for it: - What happens on your side if the decision slips past the 28th? | - What does the competing quote include, and how does its scope compare with what the pilot covered? | - How will the CFO compare this against renewing the spreadsheet-and-contractor setup for another year? | - What would success look like for your maintenance team this year? | - How would Priya's team measure the pilot results once the platform runs across all 1,400 trucks? | - What would the CFO need to see to sign before the budget closes?
The page's check: kept - nothing to flag
respond - A vendor in an enterprise deal
What they said is the sample's own line. Private figures: $180k (escalation, Price), $160k (rupture, Price), 12 months (rupture, Term), 60 days (escalation, Payment terms), 90 days (rupture, Payment terms).
Input (the situation is 1096 characters; the first 885 are shown):
{
"task": "respond",
"situation": "Domain: B2B sales\nStage: Mid-negotiation\nThe situation: We sell Corvane, a fleet-maintenance platform, to Halvorine Freight, a 1,400-truck carrier. A 200k ACV deal in its third month; procurement is running the RFP and the CFO signs. Losing it misses our quarter; winning it is our largest logistics logo.\nCounterparty: Dana Okarvo, procurement lead (in the room); Priya Shandrel, head of maintenance and our champion; the CFO, off-stage, signs\nWhat they have said, verbatim:\n- \"Your competitor quoted us half that.\"\n- \"We need a decision by the 28th, our budget closes.\"\n- \"Maintenance loves the pilot results.\"\nAuthority - what can be committed without checking: Up to 10% discount and payment terms to 45 days without asking anyone\nBATNA - the next best option if this fails: Two other carriers in late stage this quarter, smaller deals\nTheir likely alternative: Renew their current\n\n[... 211 more characters cut from this display ...]",
"mandate": "Price (more is better): entry $240k; ideal $220k; objective $200k; escalation $180k; rupture $160k; their stated position $150k; their estimated limit $175k\nTerm (more is better): entry 36 months; ideal 36 months; objective 24 months; escalation 24 months; rupture 12 months\nPayment terms (less is better): entry 30 days; ideal 30 days; objective 45 days; escalation 60 days; rupture 90 days\nImplementation services (more is better): entry not set; ideal not set; objective not set; escalation not set; rupture not set",
"said": "Honestly, $240k is way above what we budgeted. Your competitor offered us the same thing for half the price.",
"facts": "Intake: 6 of 6 answers given.\nMandate gaps the page found: Implementation services: no entry, ideal, objective, escalation, rupture - derive the rupture from your BATNA.\nPrice: concession ladder from the entry to the objective, shrinking 50/30/20 percent of the gap: $220k, then $208k, then $200k; a precise anchor instead of the round entry: $238.4k; zone of agreement $160k to $175k.\nTerm: concession ladder from the entry to the objective, shrinking 50/30/20 percent of the gap: 30 months, then 26 months, then 24 months.\nPayment terms: concession ladder from the entry to the objective, shrinking 50/30/20 percent of the gap: 38 days, then 42 days, then 45 days.\nPrivate figures - never in a line said to the other side: $180k (escalation, Price), $160k (rupture, Price), 12 months (rupture, Term), 60 days (escalation, Payment terms), 90 days (rupture, Payment terms).\nWhat they just said, read by the page: root price; a competing alternative named. Their mood as observed: Calm and articulate.\nmust: the reply never states a private figure, never concedes without a counter-ask, and its question starts with What or How."
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "respond",
"summary": "Dana Okarvo is pressing on price with a competitor claim you cannot verify; do not argue or move, state the scope facts cleanly and give a face-saving way to compare like for like.",
"triage": "bad-faith",
"root": "price",
"read": "Underneath, procurement needs a visible reduction to show the CFO, and the competitor claim is the lever to get it. Possibly Dana Okarvo is judged on savings against the first quote rather than on the rollout - test this.",
"reply": {
"label": "It sounds like our number is well above the budget you have, and you need something you can defend to the CFO.",
"question": "What does the other quote include, so we can compare it with the scope Priya's team piloted?",
"line": "Our proposal covers the full scope of the pilot across all 1,400 trucks, which is what the maintenance team has been testing. If the other offer turns out to cover a different scope, it might be worth putting the two side by side before [... 16 more characters cut from this display]"
},
"silence": "Stop after the question and count to four in silence - let Dana Okarvo describe the other quote.",
"trade": {
"give": "",
"get": ""
},
"avoid": [
"Dropping the price on the spot to answer the competitor claim.",
"Challenging the other quote as false or asking Dana Okarvo to prove it.",
"Offering to split the gap between the two numbers."
],
"mandate_check": "Nothing has been given: you are still at your opening, above your objective and well clear of your escalation point.",
"caveats": [
"Do not guess the competitor's real figure or scope; let Dana Okarvo supply it.",
"If a move is needed later, trade it - payment terms or term length - rather than leading with price."
]
}
Cut from this display only (the saved reply and the check use the full text): reply.line (252 characters, 236 shown).
What step 5 prints for it: bad-faith | price | What does the other quote include, so we can compare it with the scope Priya's team piloted?
The page's check: kept - nothing to flag
respond - A hiring manager closing a candidate
Nothing has been said yet, so the input rehearses the objection the saved plan predicted (R.laneForm puts premortem.best.objection in said). Private figures: $170k (escalation, Base salary), $180k (rupture, Base salary), $15k (escalation, Sign-on bonus), $20k (rupture, Sign-on bonus), 6 weeks (escalation, Start date), 8 weeks (rupture, Start date).
Input (the situation is 918 characters; the first 896 are shown):
{
"task": "respond",
"situation": "Domain: Recruitment close\nStage: Close to agreement\nThe situation: I am the hiring manager closing Lena Parvell for a senior data engineer role on a team of six. She is our first choice after a four-week process; the role has been open three months and is blocking a platform launch.\nCounterparty: Lena Parvell, the candidate; our recruiter Tom; the HR business partner signs anything off-band\nWhat they have said, verbatim:\n- \"I'm really excited about the team.\"\n- \"My current employer will probably counter.\"\nAuthority - what can be committed without checking: Up to the band ceiling of $160k and a $10k sign-on without HR\nBATNA - the next best option if this fails: Our second candidate, a solid but more junior engineer; re-opening the search costs another two months\nTheir likely alternative: not given\nLines planned so far:\n- What would make this an easy yes for you?\n- How are you weighing\n\n[... 22 more characters cut from this display ...]",
"mandate": "Base salary (less is better): entry $150k; ideal $155k; objective $160k; escalation $170k; rupture $180k; their stated position $165k; their estimated limit $162k\nSign-on bonus (less is better): entry $0; ideal $5k; objective $10k; escalation $15k; rupture $20k\nStart date (less is better): entry 2 weeks; ideal 3 weeks; objective 4 weeks; escalation 6 weeks; rupture 8 weeks",
"said": "My current employer countered, so I would need you to get to $165k to make the move.",
"facts": "Intake: 6 of 6 answers given.\nMandate gaps the page found: none.\nBase salary: concession ladder from the entry to the objective, shrinking 50/30/20 percent of the gap: $155k, then $158k, then $160k; a precise anchor instead of the round entry: $151.1k; zone of agreement $162k to $180k.\nSign-on bonus: concession ladder from the entry to the objective, shrinking 50/30/20 percent of the gap: $5,000, then $8,000, then $10,000.\nStart date: concession ladder from the entry to the objective, shrinking 50/30/20 percent of the gap: 3 weeks, then 4 weeks.\nPrivate figures - never in a line said to the other side: $170k (escalation, Base salary), $180k (rupture, Base salary), $15k (escalation, Sign-on bonus), $20k (rupture, Sign-on bonus), 6 weeks (escalation, Start date), 8 weeks (rupture, Start date).\nWhat they just said, read by the page: root price; a competing alternative named. Their mood as observed: not said.\nmust: the reply never states a private figure, never concedes without a counter-ask, and its question starts with What or How."
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "respond",
"summary": "Lena Parvell is using the counter-offer to lift base to $165k; name the bidding pattern without challenging it, learn what the counter actually changes, and trade any move for a signed decision.",
"triage": "tactical",
"root": "price",
"read": "Underneath, Lena Parvell still wants the move but needs a reason to say no to her current employer that she can stand behind. Possibly the counter touches something beyond base, such as recognition or a bonus she would leave behind - test [... 6 more characters cut from this display]",
"reply": {
"label": "It sounds like your current employer really wants to keep you, and that makes this decision harder.",
"question": "What does their counter change for you beyond the base salary?",
"line": "I would rather not turn this into a bidding round against an offer neither of us has in front of us. If we can make the package work, I would want it to be the last conversation about money, with a signed acceptance at the end of it."
},
"silence": "Stop after the question and count to four - let Lena Parvell describe the counter before you say any figure.",
"trade": {
"give": "Move base to $158k, the next step on the ladder.",
"get": "A signed acceptance by a date Lena Parvell names, before the counter-offer conversation with her current employer goes any further."
},
"avoid": [
"Jumping straight to $165k to match the number.",
"Splitting the gap between your offer and her number.",
"Dismissing the counter-offer or warning her about counter-offers in general."
],
"mandate_check": "The trade stays inside your objective and your authority; her number sits above the band ceiling, so anything closer to it needs the HR business partner and moves you toward your escalation point.",
"caveats": [
"The page found no root objection; this is a price push carried by a competing offer, so it is treated as price.",
"Before going past the band ceiling, check with the HR business partner and weigh it against the second candidate and two more months of search.",
"A sign-on bonus within your authority can close part of the gap without lifting base."
]
}
Cut from this display only (the saved reply and the check use the full text): read (244 characters, 238 shown).
What step 5 prints for it: tactical | price | What does their counter change for you beyond the base salary?
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./negkit.js"); require("./negreply.js")), then:
const K = NegKit, R = NegReply;
const f = K.form(Object.assign(K.blank(), { domain: "salary", stage: "early",
situation: "Asking my manager for a raise before the review cycle; I lead the billing rewrite and my base is below the band midpoint.",
counterparty: "My manager; the director approves off-cycle", quotes: "Budgets are tight this year.", authority: "My own yes or no",
batna: "A standing offer elsewhere at a higher base", axes: "Base salary | higher | $142k | $138k | $134k | $129k | $127k" }));
// f.axesA (the ladder, the anchor, the zone), f.gaps, K.privateFigures(f.axesP), f.findings
const b = K.buildInput(f, "prepare"); // or "respond" (set said, or R.laneForm(form, "respond", plan))
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "prepare");
// R.check(reply, b) // {status: kept or flagged, notes}
// R.card(reply) // the card for the room, no private figures
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, 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://walk-away.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 = "YOUR_TOKEN" # from https://walk-away.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"]
// walk-away.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://walk-away.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 = "YOUR_TOKEN" // from https://walk-away.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
}
// Walk Away.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 Walk Away {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://walk-away.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 = "YOUR_TOKEN" # from https://walk-away.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 = "YOUR_TOKEN"; // from https://walk-away.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 = "YOUR_TOKEN"; // from https://walk-away.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":"walk-away"}' | 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": "walk-away"})["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: "walk-away" })).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": "walk-away"}, 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", "walk-away"), 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: "walk-away" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "walk-away"])["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 = "walk-away" }))!["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": "prepare", "situation": "Domain: B2B sales\nStage: Mid-negotiation\n...", "mandate": "Price (more is better): entry $240k; ...", "said": "", "facts": "Intake: 6 of 6 answers given. ..."}
# - see the input 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": "prepare", "situation": "Domain: B2B sales\nStage: Mid-negotiation\n...", "mandate": "Price (more is better): entry $240k; ...", "said": "", "facts": "Intake: 6 of 6 answers given. ..."}
# - see the input 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: "prepare" or "respond", situation, mandate, said, facts} (see the input 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 walk-away:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A prepare reply carries summary, gaps, pos, anchor, ladder, questions, labels, sco, pause, premortem, back_brief and caveats; a respond reply carries summary, triage, root, read, reply, silence, trade, avoid, mandate_check and caveats. The code below prints the gist of either: the calibrated questions, or the triage, the root and the question to ask.
KEY="walk-away:$(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 == "respond" then "\(.triage) | \(.root) | \(.reply.question)"
else (.questions[] | "- \(.)") end' reply.json
import hashlib, time
key = "walk-away:" + 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["task"])
if reply["task"] == "respond":
print(reply["triage"], "|", reply["root"], "|", reply["reply"]["question"])
else:
for q in reply["questions"]:
print("-", q)
import { createHash } from "node:crypto";
const key = `walk-away:${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.task);
if (reply.task === "respond") console.log(reply.triage, "|", reply.root, "|", reply.reply.question);
else for (const q of reply.questions) console.log("-", q);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "walk-away:" + 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)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
if reply["task"] == "respond" {
rp, _ := reply["reply"].(map[string]any)
fmt.Println(reply["triage"], "|", reply["root"], "|", rp["question"])
} else {
qs, _ := reply["questions"].([]any)
for _, q := range qs {
fmt.Println("-", q)
}
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "walk-away:" + 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.path("task").asText());
if (reply.path("task").asText().equals("respond")) {
System.out.println(reply.path("triage").asText() + " | " + reply.path("root").asText() + " | " + reply.path("reply").path("question").asText());
} else {
for (JsonNode q : reply.path("questions")) System.out.println("- " + q.asText());
}
require "digest"
key = "walk-away:#{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["task"]].inspect
if reply["task"] == "respond"
puts "#{reply["triage"]} | #{reply["root"]} | #{reply.dig("reply", "question")}"
else
reply["questions"].each { |q| puts "- #{q}" }
end
$key = "walk-away:" . $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["task"], "\n";
if ($reply["task"] === "respond") { echo $reply["triage"], " | ", $reply["root"], " | ", $reply["reply"]["question"] ?? "", "\n"; }
else { foreach ($reply["questions"] as $q) echo "- ", $q, "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"walk-away:{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>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "respond") Console.WriteLine($"{reply["triage"]} | {reply["root"]} | {reply["reply"]?["question"]}");
else { foreach (var q in reply["questions"]!.AsArray()) Console.WriteLine($"- {q}"); }
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: walk-away:$(jq -r .task input.json):$(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": "prepare", "situation": "Domain: B2B sales\nStage: Mid-negotiation\n...", "mandate": "Price (more is better): entry $240k; ...", "said": "", "facts": "Intake: 6 of 6 answers given. ..."}
# - see the input 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": "walk-away:" + rev["task"] + ":" + 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": `walk-away:${rev.task}:${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", "walk-away:"+input["task"].(string)+":"+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", "walk-away:" + input.get("task") + ":" + 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" => "walk-away:#{JSON.parse(rev)["task"]}:#{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: walk-away:" . json_decode($rev, true)["task"] . ":" . 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", $"walk-away:{input["task"]}:{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);