Run OKR Desk from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page checks the OKR set for free - key results that are deliverables or activity counts, no target, a target without a baseline, no evidence source or deadline, numbers in the objective, more than three objectives, growth with no guardrail, individual OKRs and scores tied to pay - and, once actual values are in, scores every key result the way its type demands (aspirational on the 0 to 1 scale, committed pass or fail, compliance met or not met, operational health against its band, learning validated or not), with guardrails kept apart and open windows deferred. That is okscan.js, and every finding carries its rule id, line and fix. The run rewrites the set into outcome-based OKRs (or drafts one from a strategy or a cycle review), or grades the closed cycle; the page then holds the reply to its own check (okreply.js): it re-checks a returned set, refuses a baseline or target marked given that is not in the input, and holds every score to its own arithmetic unless the reply disputes it.
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":"okr-desk"}), so no slug header is needed afterwards. Send it as Authorization: Bearer .... The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a field the run needs (task, facts) is missing or not a string. Send every field as a string; okrs and context may be "". |
| 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 - "write" (the OW- rules of the OKR writer: a verdict on the pasted set, findings with fixes, a call on every scan finding, and the whole set rewritten, with an eleven-criterion quality audit) and "grade" (the OG- rules of the OKR grader: a verdict on the closed cycle, a scorecard with one entry per key result, a read per objective, guardrails, evidence problems, initiatives, learnings and next-cycle calls). OK- rules belong to both. A missing or unknown task is answered as write, with an open question saying so. Both lanes send the same fields.
The input
Every field is a string.
| Field | Required | What |
|---|---|---|
task | yes | "write" or "grade". The model answers that task and only that task. |
okrs | no | the OKR set as the team wrote it, in the input format. The page sends one team's set of up to 20,000 characters. A "grade" run needs actual values on the key results (| actual: 26%). In "write" it may be "": the run then drafts a set from context. |
context | no | the strategy, the team and what is already committed - and, after a grading, the cycle review's learnings and next-cycle calls (see next cycle). Up to 6,000 characters on the page. May be empty; when given, the summary says how it changed the answer. |
facts | yes | the page's own check of the same set: a header, every key result with its type, indicator, baseline, target, deadline, source, actual and the page's score with its calculation, a read per objective, the initiatives, and every finding as rule | severity | line | KR | text. See the facts format. |
retry_note | no | present 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 input format
okrs is plain text, one item per line. The parts the check reads:
- Header lines, before the first objective:
Team:,Cycle:(alsoQuarter:orPeriod:),Scope:,Source of truth:(orTracker:),Strategy:(orParent OKR:) andEmpowerment:. - Objectives as
O1: ...,O2: ...- qualitative, with no numbers in them. - Key results as
KR1.1: ...(or a bullet under an objective), each withkey: valueattributes after|on the same line or on the lines below:type(committed, aspirational, learning, operational_health, compliance_or_safety),indicator(leading, lagging, guardrail, health, evidence_generation),baseline,target,deadline,source,owner,confidence,actual,toleranceandoriginal target. - The statement itself is read for
from A to B,below X,at least X,between X and Yandby DATE, so a key result written plainly needs no baseline or target attribute. - An
Initiatives:line, then one bullet per initiative:- Lifecycle emails -> KR1.1 | shipped(the key results it is a bet on, and optionally its status).
Team: Customer Support
Cycle: 2026-Q3
Source of truth: Support OKR sheet, Q3 tab
O1: Customers get their problem solved the first time they ask
KR1.1: Raise first-contact resolution from 62% to 72% by 2026-09-30 | type: aspirational | indicator: lagging | source: helpdesk resolution report | actual: 69%
KR1.3: Customer satisfaction on solved tickets stays at or above 90% | type: operational_health | indicator: guardrail | source: post-ticket CSAT survey | actual: 91.5%
Initiatives:
- Macro library rewrite -> KR1.1 | shipped
An open window is written as an actual such as not yet observable (60-day interim 19%); a key result with no type is scored as aspirational (a guardrail as operational_health) and flagged.
The facts format
facts is the page's own check, written as plain text. The page builds it from the set with okscan.js, and the model scores against it: in "grade" each scorecard score is the page score from the facts unless the reply disputes it (call: "disagree", with the reason), and each objective's average is the page read's average. A client calling the API should send its own summary of the set - the team, the cycle, each key result with its type, baseline, target, actual and the score you computed, and what you already know is wrong - or, better, the same per-KR line format (from okscan.js run under node - see Build the input yourself). The check's format gets the most out of a run: the model answers every error and warn rule id of its lane's family (and every OK- id) it finds there, with one prescan entry per id, and uses the KR ids it lists. The format, line by line:
Set: <team>, <cycle|cycle not stated>; <n> objectives, <n> key results, <n> initiatives; <source of truth: ...|no source of truth named>.
Strategic input: <only when a Strategy: line is given>.
Empowerment (as written): <only when an Empowerment: line is given>.
Actual values: <present - the cycle can be graded.|none - this is a draft or a plan, not a closed cycle.>
Prescan: write verdict <ready|revise|rework|none> (<n> errors, <n> warnings); grade rules <n> errors, <n> warnings.
O1: <objective>
page read: aspirational primary average <0.72 over 2 KRs|none>; guardrails apart: <KR1.3 pass|none>; committed misses: <...|none>; deferred: <...|none>.
- KR1.1 (O1) | type <type>[ [default]] | indicator <...> | baseline <...> | target <...> | deadline <...> | source <...> | actual <...> | page score <label> [<calculation>]
<the key result as written>
Initiatives:
- <name> -> <KR ids|no KR named>[ | <status>]
Findings (rule | severity | line | KR | text):
- <rule id> | <error|warn|info> | line <n> | <KR id|-> | <title>[ (note)][ | the source line]
A value the set does not give is written -. The page read line and the actual / page score parts appear only when actual values are present. A set with no findings gets the single line - none, and an empty okrs gets the whole facts No OKR set pasted - draft one from the context. Both lanes receive the same facts: OW- findings belong to write, OG- findings to grade, OK- to both. The prescan verdict is rework at two write-lane errors, or one error with errors or warnings in three of the areas (outcomes, measurement, objectives, focus, guardrails, integrity, scores, evidence); revise on any write-lane error or warning; ready otherwise. A real example - the facts the page sends for the support team's Q3 set used in the worked example below:
Set: Customer Support, 2026-Q3; 2 objectives, 5 key results, 3 initiatives; source of truth: Support OKR sheet, Q3 tab.
Strategic input: Cut the cost of a solved problem without making customers wait or feel brushed off.
Actual values: present - the cycle can be graded.
Prescan: write verdict ready (0 errors, 0 warnings); grade rules 0 errors, 0 warnings.
O1: Customers get their problem solved the first time they ask
page read: aspirational primary average 0.72 over 2 KRs; guardrails apart: KR1.3 pass; committed misses: none; deferred: none.
- KR1.1 (O1) | type aspirational | indicator lagging | baseline 62% | target 72% | deadline 2026-09-30 | source helpdesk resolution report | actual 69% | page score 0.70 [(69% - 62%) / (72% - 62%) = 0.70]
Raise first-contact resolution from 62% to 72% by 2026-09-30
- KR1.2 (O1) | type aspirational | indicator leading | baseline 4.2 h | target 2 h | deadline 2026-09-30 | source helpdesk SLA report | actual 2.6 h | page score 0.73 [(2.6 h - 4.2 h) / (2 h - 4.2 h) = 0.73]
Cut median time to first reply from 4.2 h to 2 h by 2026-09-30
- KR1.3 (O1) | type operational_health | indicator guardrail | baseline - | target at least 90% | deadline - | source post-ticket CSAT survey | actual 91.5% | page score pass [91.5% against at least 90%]
Customer satisfaction on solved tickets stays at or above 90%
O2: Answers customers can find without writing to us
page read: aspirational primary average 0.67 over 1 KR; guardrails apart: none; committed misses: none; deferred: none.
- KR2.1 (O2) | type aspirational | indicator leading | baseline 31% | target 40% | deadline 2026-09-30 | source help center analytics | actual 37% | page score 0.67 [(37% - 31%) / (40% - 31%) = 0.67]
Increase help-center self-serve rate from 31% to 40% by 2026-09-30
- KR2.2 (O2) | type learning | indicator evidence_generation | baseline - | target - | deadline - | source A/B test on the billing contact form | actual validated - 11% fewer billing tickets in the test arm | page score validated [learning KR - graded by what it settled]
Learn whether in-product answer suggestions deflect billing tickets
Initiatives:
- Macro library rewrite -> KR1.1 | shipped
- Follow-the-sun shift pattern -> KR1.2 | shipped
- Suggested answers in the contact form -> KR2.1, KR2.2 | shipped
Findings (rule | severity | line | KR | text):
- none
Rule ids the check uses, with their severity and lane:
| Rule | Severity | Lane | Title |
|---|---|---|---|
OK-COMP | error | both | OKR scores tied to pay or performance ratings |
OW-OUTPUT | error | write | Key result is a deliverable, not an outcome |
OW-TASKCOUNT | warn | write | Key result counts activity |
OW-NOTARGET | error | write | No target |
OW-NOBASE | warn | write | Target without a baseline |
OW-NOSOURCE | warn | write | No evidence source |
OW-NODATE | info | write | No deadline or cycle |
OW-OBJMETRIC | warn | write | Numbers in the objective |
OW-VAGUEOBJ | info | write | Objective leans on empty adjectives |
OW-FOCUS | warn | write | Too many (or too few) objectives or key results |
OW-NOGUARD | warn | write | Growth or speed key result with no guardrail |
OW-INDIVIDUAL | warn | write | Individual OKRs |
OW-NOTYPE | info | write | Key result type not stated |
OG-NOACTUAL | warn | grade | No actual value to grade |
OG-COMMITMISS | error | grade | Committed key result missed |
OG-GUARDFAIL | error | grade | Guardrail broken |
OG-LOWSTRETCH | warn | grade | Aspirational key result below 0.4 |
OG-PARTIAL | warn | grade | Committed scope only partly covered |
OG-RETARGET | warn | grade | Target or baseline changed mid-cycle |
OG-SANDBAG | info | grade | Aspirational key result above 0.85 |
OG-DEFERRED | info | grade | Not yet observable |
A finding the check cannot see (a vanity metric, a key result the team cannot influence, cascading theater, a lag-only set, confounded initiatives) comes back with the rule NEW.
The standard the model applies
The two source skills - @product-on-purpose/foundation-okr-writer (the write lane) and @product-on-purpose/measure-okr-grader (the grade lane), from product-on-purpose/pm-skills (Apache-2.0) - are the only standard:
- Outcomes, not output: OKRs are a focus and learning system, not a project plan or a roadmap wrapper. Objectives are qualitative and directional; key results measure a change in customer behaviour, a business KPI or operational health - never features, tasks or activity counts. Features go to Initiatives, each naming the key result it is a bet on.
- Measurement: every key result has a metric, a baseline (or
recommended-to-measure), a target, a deadline, an evidence source, an indicator class, a confidence and a type. A number that is not in the input is never invented: it is markedassumptionorplaceholderand asked as an open question. - Focus and guardrails: 1 to 3 objectives with 2 to 4 key results each; every growth, speed or volume key result gets a guardrail with a threshold it must hold; team OKRs by default.
- Integrity: OKR scores are never tied to pay, bonuses, promotions or performance ratings. Pre-committed work (a feature-team or mixed set) is framed as outcome bets, with the standard Disclosure.
- Scoring by type: aspirational is (actual - baseline) / (target - baseline), sweet spot 0.6 to 0.7, below 0.4 a miss, above 0.85 a sign of sandbagged targets; committed is pass or fail; compliance_or_safety is met or not met; operational_health is pass, fail or drift-within-tolerance; learning is validated, invalidated, partially-validated or insufficient-evidence.
- Guardrails are reported on their own and never averaged into the objective; an open window is
deferred; partial coverage of a committed key result isnot-yet-fully-observable; a target changed mid-cycle is graded against both; shipping an initiative that did not move its key result is not a partial win.
House rules
- Every statement is grounded in the input; no team, metric, number, dashboard or event is invented. What the answer needs but could not see goes in
open_questions(write) orrisks(grade). - Every
errorandwarnfinding of the lane's family in the facts (and everyOK-one) is answered by its rule id inprescan; a finding the model thinks is wrong is disputed with a reason, never silently dropped. - Key results are named by the ids in the facts (
KR1.1,KR2.3);wherenames a KR or objective id. - The OKR tracker is the source of truth, not the reply:
source_of_truthnames it, or says it is not named. - The reply is ONE JSON object and nothing else - no prose around it, no Markdown fences.
The replies
Both lanes carry verdict, summary, findings (rule, severity high / medium / low, title, where, why, fix) and prescan (rule, call agree or disagree, note). The rest differs by lane.
write
verdict on the pasted set: ready (nothing in it fails the rules), revise or rework (output key results, no targets, compensation coupling, a roadmap in OKR clothes). mode is rewrite (a set was pasted), draft (only a strategy in context) or next-cycle (the context carries a cycle review).
summary- two or three sentences: what the set is for, the verdict, the biggest change.empowerment-empowered,feature-team,mixedorunknown;disclosureis the Disclosure text forfeature-teamormixed, and""otherwise.findingsandprescan-OW-andOK-rule ids from the facts, orNEW.set- the whole set to use:context(team,cycle,scope,source_of_truth,strategy),objectives(1 to 3, eachid,titleand 2 to 4krs) andinitiatives(name,krs,hypothesis). Each key result:id,statement,type,indicator,baselineandtargetwithbaseline_basisandtarget_basis(given,assumptionorplaceholder),deadline,source,owner,confidence.audit- eleven entries in this order: Strategic fit, Objective quality, KR outcome quality, Measurement quality, Product influence, Focus, Guardrails, Alignment, Operating rhythm, Integrity, Empowered-team Disclosure - each withratingpass,riskorfail(not-applicableonly for the Disclosure when empowered) and a one-linewhy.open_questions(every assumption and placeholder introduced),next_step(one sentence),caveats.
{
"task": "write",
"mode": "rewrite",
"verdict": "revise",
"empowerment": "empowered",
"summary": "",
"findings": [{"rule": "OW-OUTPUT", "severity": "high", "title": "", "where": "KR1.1", "why": "", "fix": ""}],
"prescan": [{"rule": "OW-OUTPUT", "call": "agree", "note": ""}],
"set": {"context": {"team": "", "cycle": "", "scope": "team", "source_of_truth": "", "strategy": ""}, "objectives": [{"id": "O1", "title": "", "krs": [{"id": "KR1.1", "statement": "", "type": "aspirational", "indicator": "leading", "baseline": "", "baseline_basis": "given", "target": "", "target_basis": "assumption", "deadline": "", "source": "", "owner": "", "confidence": "medium"}]}], "initiatives": [{"name": "", "krs": ["KR1.1"], "hypothesis": ""}]},
"audit": [{"criterion": "Strategic fit", "rating": "pass", "why": ""}],
"disclosure": "",
"open_questions": [""],
"next_step": "",
"caveats": [""]
}
grade
verdict on the closed cycle: hit (every committed and compliance key result met, guardrails held, aspirational scores in or above the sweet spot), mixed, or missed (committed misses or broken guardrails the primary scores cannot excuse).
summary- two or three sentences: what the cycle was for, the verdict, the one thing next cycle must change.scorecard- one entry per key result in the facts:kr,actual,score(the page's label unless disputed),call,confidence,interpretation.objectives-id,read(qualitative, never a naive average over guardrails, learning or deferred key results),average(as the facts give it, ornull).guardrails(kr,signal,note) andevidence(kr,issue,fix).initiatives-name,krs,shipped(yes/no/partial),moved(yes/no/unclear, kept apart from shipping),verdict(continue/rework/retire),note.learning(validated,invalidated,surprises) andnext_cycle- one per objective:objective,action(continue/revise/retire/escalate),why,draft.risks,findingsandprescan(OG-andOK-rule ids, orNEW),source_of_truth.
{
"task": "grade",
"verdict": "mixed",
"summary": "",
"scorecard": [{"kr": "KR1.1", "actual": "", "score": "0.86", "call": "agree", "confidence": "high", "interpretation": ""}],
"objectives": [{"id": "O1", "read": "", "average": 0.86}],
"guardrails": [{"kr": "KR1.3", "signal": "fail", "note": ""}],
"evidence": [{"kr": "", "issue": "", "fix": ""}],
"initiatives": [{"name": "", "krs": ["KR1.1"], "shipped": "yes", "moved": "yes", "verdict": "continue", "note": ""}],
"learning": {"validated": [""], "invalidated": [""], "surprises": [""]},
"next_cycle": [{"objective": "O1", "action": "continue", "why": "", "draft": ""}],
"risks": [""],
"findings": [{"rule": "NEW", "severity": "medium", "title": "", "where": "KR1.2", "why": "", "fix": ""}],
"prescan": [{"rule": "OG-GUARDFAIL", "call": "agree", "note": ""}],
"source_of_truth": ""
}
The page holds either reply to its own check: every error and warning of the lane's family answered by rule id, no rule id the check did not report other than NEW, no ready or hit over an undisputed error. For write it writes the returned set back in the input format and re-checks it (an error still present or newly introduced is a note), refuses a baseline or target marked given that is not in the input, wants an open question for every assumption, all eleven audit criteria and the Disclosure exactly when the work is pre-committed. For grade it wants every key result scored, each score equal to its own (or disputed), no number on a deferred, committed or compliance key result, and each objective's average equal to its read. Each miss is shown next to the reply as a note.
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 okreply.js). 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 'grade' - no prose, no code fences." and a new attempt number in the key (see step 5). In "write", the set can be written back in the input format - header lines from set.context, an O1: line per objective, a KR1.1: statement | type: ... | indicator: ... line per key result, and an Initiatives: list - which is what the page shows and re-checks, and what step 5 prints; that text can go straight back in as okrs. Confirm every baseline or target whose basis is assumption or placeholder before the cycle starts. In "grade", the scorecard is the answer; the OKR tracker named in source_of_truth stays the record.
Drafting a set, and the next cycle
A "write" run with an empty okrs drafts a set from context alone (mode: "draft"): describe the strategy, the team and what is already committed in context, and send facts as No OKR set pasted - draft one from the context. (the page asks for at least a sentence or two of context). After a "grade" run, the grading's learnings can be passed as context to a "write" run, with the graded set as okrs or with okrs empty: the reply comes back as mode: "next-cycle" and says which recommendations it followed. The page builds that context from the grade reply, one line each: Cycle review of ... (OKR Desk grade, verdict hit): <summary>, Validated: ..., Invalidated: ..., Surprises: ..., Next cycle O1: <action> - <why> Candidate: <draft> and Initiative <name>: <verdict> - <note>, clipped to fit the 6,000-character context.
Worked example - grading the support team's Q3
A clean closed cycle: two objectives, three aspirational key results (first-contact resolution 62% to 72%, actual 69%; median first reply 4.2 h to 2 h, actual 2.6 h; self-serve rate 31% to 40%, actual 37%), a CSAT guardrail that must stay at or above 90% (actual 91.5%), a learning key result about answer suggestions, and three shipped initiatives. The check finds nothing wrong, and scores 0.70, 0.73, pass, 0.67 and validated. The input below is exactly what the page sends for the "grade" lane; for the "write" lane only task changes to "write", and the same facts go along. Input:
{
"task": "grade",
"okrs": "Team: Customer Support\nCycle: 2026-Q3\nSource of truth: Support OKR sheet, Q3 tab\nStrategy: Cut the cost of a solved problem without making customers wait or feel brushed off\n\nO1: Customers get their problem solved the first time they ask\nKR1.1: Raise first-contact resolution from 62% to 72% by 2026-09-30 | type: aspirational | indicator: lagging | source: helpdesk resolution report | actual: 69%\nKR1.2: Cut median time to first reply from 4.2 h to 2 h by 2026-09-30 | type: aspirational | indicator: leading | source: helpdesk SLA report | actual: 2.6 h\nKR1.3: Customer satisfaction on solved tickets stays at or above 90% | type: operational_health | indicator: guardrail | source: post-ticket CSAT survey | actual: 91.5%\n\nO2: Answers customers can find without writing to us\nKR2.1: Increase help-center self-serve rate from 31% to 40% by 2026-09-30 | type: aspirational | indicator: leading | source: help center analytics | actual: 37%\nKR2.2: Learn whether in-product answer suggestions deflect billing tickets | type: learning | indicator: evidence_generation | source: A/B test on the billing contact form | actual: validated - 11% fewer billing tickets in the test arm\n\nInitiatives:\n- Macro library rewrite -> KR1.1 | shipped\n- Follow-the-sun shift pattern -> KR1.2 | shipped\n- Suggested answers in the contact form -> KR2.1, KR2.2 | shipped",
"context": "",
"facts": "Set: Customer Support, 2026-Q3; 2 objectives, 5 key results, 3 initiatives; source of truth: Support OKR sheet, Q3 tab.\nStrategic input: Cut the cost of a solved problem without making customers wait or feel brushed off.\nActual values: present - the cycle can be graded.\nPrescan: write verdict ready (0 errors, 0 warnings); grade rules 0 errors, 0 warnings.\nO1: Customers get their problem solved the first time they ask\n page read: aspirational primary average 0.72 over 2 KRs; guardrails apart: KR1.3 pass; committed misses: none; deferred: none.\n- KR1.1 (O1) | type aspirational | indicator lagging | baseline 62% | target 72% | deadline 2026-09-30 | source helpdesk resolution report | actual 69% | page score 0.70 [(69% - 62%) / (72% - 62%) = 0.70]\n Raise first-contact resolution from 62% to 72% by 2026-09-30\n- KR1.2 (O1) | type aspirational | indicator leading | baseline 4.2 h | target 2 h | deadline 2026-09-30 | source helpdesk SLA report | actual 2.6 h | page score 0.73 [(2.6 h - 4.2 h) / (2 h - 4.2 h) = 0.73]\n Cut median time to first reply from 4.2 h to 2 h by 2026-09-30\n- KR1.3 (O1) | type operational_health | indicator guardrail | baseline - | target at least 90% | deadline - | source post-ticket CSAT survey | actual 91.5% | page score pass [91.5% against at least 90%]\n Customer satisfaction on solved tickets stays at or above 90%\nO2: Answers customers can find without writing to us\n page read: aspirational primary average 0.67 over 1 KR; guardrails apart: none; committed misses: none; deferred: none.\n- KR2.1 (O2) | type aspirational | indicator leading | baseline 31% | target 40% | deadline 2026-09-30 | source help center analytics | actual 37% | page score 0.67 [(37% - 31%) / (40% - 31%) = 0.67]\n Increase help-center self-serve rate from 31% to 40% by 2026-09-30\n- KR2.2 (O2) | type learning | indicator evidence_generation | baseline - | target - | deadline - | source A/B test on the billing contact form | actual validated - 11% fewer billing tickets in the test arm | page score validated [learning KR - graded by what it settled]\n Learn whether in-product answer suggestions deflect billing tickets\nInitiatives:\n- Macro library rewrite -> KR1.1 | shipped\n- Follow-the-sun shift pattern -> KR1.2 | shipped\n- Suggested answers in the contact form -> KR2.1, KR2.2 | shipped\nFindings (rule | severity | line | KR | text):\n- none"
}
Reply - the real saved reply the page shows for this sample, abbreviated: long strings are cut and end in ..., and evidence, initiatives, risks and findings keep only their first entries (the full reply has 3 evidence entries, 3 initiatives, 4 risks and 3 findings):
{
"task": "grade",
"verdict": "hit",
"summary": "Customer Support's 2026-Q3 cycle aimed to cut the cost of a solved problem without making customers wait or feel brushed off. Verdict: hit - the three aspirational KRs scored 0.70, 0.73 and 0.67, inside or just above the 0.6 to 0.7 sweet spot, the CSAT ...",
"scorecard": [
{
"kr": "KR1.1",
"actual": "69%",
"score": "0.70",
"call": "agree",
"confidence": "high",
"interpretation": "First-contact resolution rose from 62% to 69% against a 72% target: (69 - 62) / (72 - 62) = 0.70, in the sweet spot for an aspirational KR. It says ..."
},
{
"kr": "KR1.2",
"actual": "2.6 h",
"score": "0.73",
"call": "agree",
"confidence": "high",
"interpretation": "Median time to first reply fell from 4.2 h to 2.6 h against a 2 h target: (2.6 - 4.2) / (2 - 4.2) = 0.73, just above the sweet spot and far below the ..."
},
{
"kr": "KR1.3",
"actual": "91.5%",
"score": "pass",
"call": "agree",
"confidence": "medium",
"interpretation": "CSAT on solved tickets ended at 91.5%, above the 90% floor, so the guardrail held while O1 pushed speed and resolution. With no baseline recorded, it ..."
},
{
"kr": "KR2.1",
"actual": "37%",
"score": "0.67",
"call": "agree",
"confidence": "high",
"interpretation": "Help-center self-serve rate rose from 31% to 37% against a 40% target: (37 - 31) / (40 - 31) = 0.67, in the sweet spot. A higher self-serve rate ..."
},
{
"kr": "KR2.2",
"actual": "validated - 11% fewer billing tickets in the test arm",
"score": "validated",
"call": "agree",
"confidence": "medium",
"interpretation": "The A/B test on the billing contact form settled the question: suggested answers deflected billing tickets, with 11% fewer in the test arm. This is a ..."
}
],
"objectives": [
{
"id": "O1",
"read": "Strong cycle: resolution and reply speed both moved most of the way to stretch targets (0.70 and 0.73) while CSAT on solved tickets held above its ...",
"average": 0.72
},
{
"id": "O2",
"read": "Good progress with real learning: self-serve rose 31% to 37% (0.67) and the deflection experiment validated its hypothesis for billing. The KR2.2 ...",
"average": 0.67
}
],
"guardrails": [
{
"kr": "KR1.3",
"signal": "pass",
"note": "91.5% against a floor of at least 90%; held while O1 pushed reply speed and first-contact resolution. It surveys solved tickets only, so it does not ..."
}
],
"evidence": [
{
"kr": "KR1.3",
"issue": "No baseline: the floor is known but the starting CSAT is not, so a drop that stays above 90% would be invisible.",
"fix": "Record CSAT on solved tickets at cycle start next to the floor."
}
],
"initiatives": [
{
"name": "Macro library rewrite",
"krs": [
"KR1.1"
],
"shipped": "yes",
"moved": "yes",
"verdict": "continue",
"note": "Shipped, and first-contact resolution moved from 62% to 69%. That is apparent contribution, not causal proof: there was no control, and the ..."
}
],
"learning": {
"validated": [
"In-product answer suggestions on the billing contact form deflect billing tickets: 11% fewer in the test arm of the A/B test (KR2.2)."
],
"invalidated": [],
"surprises": [
"None recorded in the input; every scored KR landed between 0.67 and 0.73 and the guardrail held."
]
},
"next_cycle": [
{
"objective": "O1",
"action": "continue",
"why": "Both KRs moved most of the way and remain short of target (69% vs 72%, 2.6 h vs 2 h) with CSAT intact, so the objective still has room and the bets ...",
"draft": "Keep O1; carry first-contact resolution from 69% and median first reply from 2.6 h as next cycle's baselines, set new targets with the team, and keep ..."
},
{
"objective": "O2",
"action": "revise",
"why": "The learning KR is settled, so it should give way to an outcome KR on the validated bet, and the self-serve push needs its own guardrail.",
"draft": "Replace KR2.2 with an outcome KR on ticket deflection from suggested answers beyond the billing form (baseline to be measured before a target is ..."
}
],
"risks": [
"A 0.73 above the 0.6 to 0.7 band could read as a beaten target; KR1.2 still missed 2 h, and 0.73 is well short of the 0.85 sandbagging signal."
],
"findings": [
{
"rule": "NEW",
"severity": "medium",
"title": "Self-serve push has no guardrail of its own",
"where": "KR2.1",
"why": "Self-serve rate is a volume push and the strategy warns against customers feeling brushed off; KR1.3 surveys solved tickets only, so it cannot see a ...",
"fix": "Add a guardrail on whether self-serve sessions solved the problem next cycle, baseline measured first."
}
],
"prescan": [],
"source_of_truth": "Support OKR sheet, Q3 tab"
}
Sent to "write", the same set comes back ready in mode: "rewrite", essentially as written: the blanks the format requires are filled (a deadline on KR1.3 and KR2.2, recommended-to-measure for the CSAT baseline, a team owner) with open questions asking the team to confirm them. Passing this grading's learnings as context instead makes it a next-cycle run, carrying the grading's calls: replace KR2.2 with an outcome key result and give the self-serve push a guardrail of its own.
Build the input yourself
The checker and the reply checker are plain scripts that run under node (global.window = global; require("./okscan.js"); require("./okreply.js")), then:
const S = OkScan, R = OkReply;
const st = { okrs: fs.readFileSync("okrs.txt", "utf8"), context: "" };
const scan = S.scan(st); // findings, counts, byLane, verdict, krs with their scores, reads
const b = S.buildInput(st, "grade"); // or "write"; b.ok is false (with b.note) when the lane cannot run
const wire = S.toWire(b.input); // task, okrs, context, facts - all strings; POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "grade");
// R.check(reply, b) // {notes, warn}: anything the reply missed or got wrong
// R.sheet(reply) // a write reply's set in the input format
// R.handoff(reply, "Support Q3") // a grade reply as the context of a next-cycle write run
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://okr-desk.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://okr-desk.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"]
// okr-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://okr-desk.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://okr-desk.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
}
// OkrDesk.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 OkrDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://okr-desk.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://okr-desk.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, 443, 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://okr-desk.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://okr-desk.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":"okr-desk"}' | 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": "okr-desk"})["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: "okr-desk" })).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": "okr-desk"}, 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", "okr-desk"), 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: "okr-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "okr-desk"])["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 = "okr-desk" }))!["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. Save the worked-example input above as input.json (with task set to "grade" or "write") to follow along.
# input.json: the run input object - {"task": "grade", "okrs": "Team: Customer Support ...", "context": "", "facts": "Set: Customer Support, 2026-Q3; ..."}
# - 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": "grade", "okrs": "Team: Customer Support ...", "context": "", "facts": "Set: Customer Support, 2026-Q3; ..."}
# - 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: "grade" or "write", okrs, context, 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 okr-desk:<task>:<inputHash>:a<attempt> - the lane is part of the key, so a write and a grade run of the same set 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. The code below parses the JSON object out of the output and prints the verdict and the summary; for "grade" it prints each key result's score and call, and for "write" it prints the returned set in the input format, ready to paste into the page or send back as okrs.
KEY="okr-desk:$(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 '"verdict: \(.verdict)", .summary,
if .task == "grade" then (.scorecard[]? | "\(.kr) \(.score) \(.call)")
else (.set.context // {} | (["Team","team"], ["Cycle","cycle"], ["Scope","scope"], ["Source of truth","source_of_truth"], ["Strategy","strategy"]) as $h
| select((.[$h[1]] // "") != "") | "\($h[0]): \(.[$h[1]])"),
(.set.objectives[]? | "", "\(.id): \(.title)", (.krs[]? | ([.statement] + [("type","indicator","baseline","target","deadline","source","owner","confidence") as $a
| select((.[$a] // "") != "") | "\($a): \(.[$a])"] | join(" | ")) as $l | "\(.id): \($l)")),
"", "Initiatives:", (.set.initiatives[]? | "- \(.name) -> \(.krs | join(", "))")
end' reply.json # write: the set back in the input format, ready to paste or send as "okrs"
import hashlib, time
key = "okr-desk:" + 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)
out = j["output"]["output"]
reply = json.loads(out[out.index("{"):out.rindex("}") + 1]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"))
print("verdict:", reply.get("verdict"))
print(reply.get("summary"))
if reply.get("task") == "grade":
for s in reply.get("scorecard", []):
print(s["kr"], s["score"], s["call"]) # the page's score label, unless the reply disputes it
else: # write: the set back in the input format, ready to paste or send as "okrs"
st = reply.get("set", {}); c = st.get("context", {})
for label, f in [("Team", "team"), ("Cycle", "cycle"), ("Scope", "scope"), ("Source of truth", "source_of_truth"), ("Strategy", "strategy")]:
if c.get(f): print(f"{label}: {c[f]}")
KEYS = ["type", "indicator", "baseline", "target", "deadline", "source", "owner", "confidence"]
for o in st.get("objectives", []):
print(f"\n{o['id']}: {o['title']}")
for k in o.get("krs", []):
print(f"{k['id']}: " + " | ".join([k["statement"]] + [f"{a}: {k[a]}" for a in KEYS if k.get(a)]))
print("\nInitiatives:")
for x in st.get("initiatives", []):
print(f"- {x['name']} -> " + ", ".join(x.get("krs", [])))
import { createHash } from "node:crypto";
const key = `okr-desk:${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 out = j.output.output;
const reply = JSON.parse(out.slice(out.indexOf("{"), out.lastIndexOf("}") + 1));
console.log(j.charged_credits, j.truncated);
console.log("verdict:", reply.verdict);
console.log(reply.summary);
if (reply.task === "grade") {
for (const s of reply.scorecard || []) console.log(s.kr, s.score, s.call); // the page's score label, unless disputed
} else { // write: the set back in the input format, ready to paste or send as "okrs"
const set = reply.set || {}, c = set.context || {};
for (const [l, f] of [["Team", "team"], ["Cycle", "cycle"], ["Scope", "scope"], ["Source of truth", "source_of_truth"], ["Strategy", "strategy"]]) if (c[f]) console.log(`${l}: ${c[f]}`);
const KEYS = ["type", "indicator", "baseline", "target", "deadline", "source", "owner", "confidence"];
for (const o of set.objectives || []) {
console.log(`\n${o.id}: ${o.title}`);
for (const k of o.krs || []) console.log(`${k.id}: ` + [k.statement, ...KEYS.filter((a) => k[a]).map((a) => `${a}: ${k[a]}`)].join(" | "));
}
console.log("\nInitiatives:");
for (const x of set.initiatives || []) console.log(`- ${x.name} -> ${(x.krs || []).join(", ")}`);
}
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "strings", "time" to the imports
key := "okr-desk:" + 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 }
}
out := j["output"].(map[string]any)["output"].(string)
var reply map[string]any
json.Unmarshal([]byte(out[strings.Index(out, "{"):strings.LastIndex(out, "}")+1]), &reply)
fmt.Println(j["charged_credits"], j["truncated"])
fmt.Println("verdict:", reply["verdict"])
fmt.Println(reply["summary"])
if reply["task"] == "grade" {
sc, _ := reply["scorecard"].([]any)
for _, x := range sc { s := x.(map[string]any); fmt.Println(s["kr"], s["score"], s["call"]) }
} else { // write: the set back in the input format, ready to paste or send as "okrs"
set, _ := reply["set"].(map[string]any)
c, _ := set["context"].(map[string]any)
for _, h := range [][2]string{{"Team", "team"}, {"Cycle", "cycle"}, {"Scope", "scope"}, {"Source of truth", "source_of_truth"}, {"Strategy", "strategy"}} {
if v, ok := c[h[1]].(string); ok && v != "" { fmt.Printf("%s: %s\n", h[0], v) }
}
objs, _ := set["objectives"].([]any)
for _, x := range objs {
o := x.(map[string]any)
fmt.Printf("\n%v: %v\n", o["id"], o["title"])
krs, _ := o["krs"].([]any)
for _, y := range krs {
k := y.(map[string]any)
parts := []string{fmt.Sprint(k["statement"])}
for _, a := range []string{"type", "indicator", "baseline", "target", "deadline", "source", "owner", "confidence"} {
if v, ok := k[a].(string); ok && v != "" { parts = append(parts, a+": "+v) }
}
fmt.Printf("%v: %s\n", k["id"], strings.Join(parts, " | "))
}
}
fmt.Println("\nInitiatives:")
inits, _ := set["initiatives"].([]any)
for _, x := range inits {
it := x.(map[string]any)
ids := []string{}
ks, _ := it["krs"].([]any)
for _, k := range ks { ids = append(ids, fmt.Sprint(k)) }
fmt.Printf("- %v -> %s\n", it["name"], strings.Join(ids, ", "))
}
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "okr-desk:" + 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()));
String out = j.get("output").get("output").asText();
JsonNode reply = M.readTree(out.substring(out.indexOf('{'), out.lastIndexOf('}') + 1));
System.out.println(j.get("charged_credits") + " " + j.get("truncated"));
System.out.println("verdict: " + reply.path("verdict").asText());
System.out.println(reply.path("summary").asText());
if ("grade".equals(reply.path("task").asText())) {
for (JsonNode s : reply.path("scorecard")) System.out.println(s.path("kr").asText() + " " + s.path("score").asText() + " " + s.path("call").asText());
} else { // write: the set back in the input format, ready to paste or send as "okrs"
JsonNode set = reply.path("set"), c = set.path("context");
String[][] head = {{"Team", "team"}, {"Cycle", "cycle"}, {"Scope", "scope"}, {"Source of truth", "source_of_truth"}, {"Strategy", "strategy"}};
for (String[] h : head) if (!c.path(h[1]).asText().isEmpty()) System.out.println(h[0] + ": " + c.path(h[1]).asText());
for (JsonNode o : set.path("objectives")) {
System.out.println();
System.out.println(o.path("id").asText() + ": " + o.path("title").asText());
for (JsonNode k : o.path("krs")) {
var parts = new ArrayList<String>(List.of(k.path("statement").asText()));
for (String a : List.of("type", "indicator", "baseline", "target", "deadline", "source", "owner", "confidence"))
if (!k.path(a).asText().isEmpty()) parts.add(a + ": " + k.path(a).asText());
System.out.println(k.path("id").asText() + ": " + String.join(" | ", parts));
}
}
System.out.println();
System.out.println("Initiatives:");
for (JsonNode x : set.path("initiatives")) {
var ids = new ArrayList<String>();
x.path("krs").forEach(k -> ids.add(k.asText()));
System.out.println("- " + x.path("name").asText() + " -> " + String.join(", ", ids));
}
}
require "digest"
key = "okr-desk:#{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
out = j["output"]["output"]
reply = JSON.parse(out[out.index("{")..out.rindex("}")])
puts [j["charged_credits"], j["truncated"]].inspect
puts "verdict: #{reply["verdict"]}", reply["summary"]
if reply["task"] == "grade"
(reply["scorecard"] || []).each { |s| puts "#{s["kr"]} #{s["score"]} #{s["call"]}" }
else # write: the set back in the input format, ready to paste or send as "okrs"
set = reply["set"] || {}
c = set["context"] || {}
{ "Team" => "team", "Cycle" => "cycle", "Scope" => "scope", "Source of truth" => "source_of_truth", "Strategy" => "strategy" }
.each { |l, f| puts "#{l}: #{c[f]}" unless c[f].to_s.empty? }
(set["objectives"] || []).each do |o|
puts "", "#{o["id"]}: #{o["title"]}"
(o["krs"] || []).each do |k|
attrs = %w[type indicator baseline target deadline source owner confidence].reject { |a| k[a].to_s.empty? }.map { |a| "#{a}: #{k[a]}" }
puts "#{k["id"]}: " + ([k["statement"]] + attrs).join(" | ")
end
end
puts "", "Initiatives:"
(set["initiatives"] || []).each { |x| puts "- #{x["name"]} -> #{(x["krs"] || []).join(", ")}" }
end
$key = "okr-desk:" . $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"]));
$out = $j["output"]["output"];
$from = strpos($out, "{");
$reply = json_decode(substr($out, $from, strrpos($out, "}") - $from + 1), true);
echo $j["charged_credits"], "\n";
echo "verdict: ", $reply["verdict"] ?? "", "\n", $reply["summary"] ?? "", "\n";
if (($reply["task"] ?? "") === "grade") {
foreach ($reply["scorecard"] ?? [] as $s) echo $s["kr"], " ", $s["score"], " ", $s["call"], "\n";
} else { // write: the set back in the input format, ready to paste or send as "okrs"
$set = $reply["set"] ?? []; $c = $set["context"] ?? [];
foreach (["Team" => "team", "Cycle" => "cycle", "Scope" => "scope", "Source of truth" => "source_of_truth", "Strategy" => "strategy"] as $l => $f)
if (!empty($c[$f])) echo "$l: ", $c[$f], "\n";
foreach ($set["objectives"] ?? [] as $o) {
echo "\n", $o["id"], ": ", $o["title"], "\n";
foreach ($o["krs"] ?? [] as $k) {
$parts = [$k["statement"]];
foreach (["type", "indicator", "baseline", "target", "deadline", "source", "owner", "confidence"] as $a)
if (!empty($k[$a])) $parts[] = "$a: " . $k[$a];
echo $k["id"], ": ", implode(" | ", $parts), "\n";
}
}
echo "\nInitiatives:\n";
foreach ($set["initiatives"] ?? [] as $x) echo "- ", $x["name"], " -> ", implode(", ", $x["krs"] ?? []), "\n";
}
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"okr-desk:{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 output = j["output"]!["output"]!.GetValue<string>();
var from = output.IndexOf('{');
var reply = JsonNode.Parse(output.Substring(from, output.LastIndexOf('}') - from + 1))!;
Console.WriteLine($"{j["charged_credits"]} {j["truncated"]}");
Console.WriteLine($"verdict: {reply["verdict"]}");
Console.WriteLine(reply["summary"]);
if ((string?)reply["task"] == "grade") {
foreach (var s in reply["scorecard"]?.AsArray() ?? new JsonArray()) Console.WriteLine($"{s!["kr"]} {s["score"]} {s["call"]}");
} else { // write: the set back in the input format, ready to paste or send as "okrs"
var set = reply["set"]!; var c = set["context"];
foreach (var (l, f) in new[] { ("Team", "team"), ("Cycle", "cycle"), ("Scope", "scope"), ("Source of truth", "source_of_truth"), ("Strategy", "strategy") })
if (!string.IsNullOrEmpty((string?)c?[f])) Console.WriteLine($"{l}: {c![f]}");
foreach (var o in set["objectives"]?.AsArray() ?? new JsonArray()) {
Console.WriteLine();
Console.WriteLine($"{o!["id"]}: {o["title"]}");
foreach (var k in o["krs"]?.AsArray() ?? new JsonArray()) {
var parts = new List<string> { (string?)k!["statement"] ?? "" };
foreach (var a in new[] { "type", "indicator", "baseline", "target", "deadline", "source", "owner", "confidence" })
if (!string.IsNullOrEmpty((string?)k[a])) parts.Add($"{a}: {k[a]}");
Console.WriteLine($"{k["id"]}: {string.Join(" | ", parts)}");
}
}
Console.WriteLine();
Console.WriteLine("Initiatives:");
foreach (var x in set["initiatives"]?.AsArray() ?? new JsonArray())
Console.WriteLine($"- {x!["name"]} -> {string.Join(", ", (x["krs"]?.AsArray() ?? new JsonArray()).Select(k => (string?)k))}");
}
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: okr-desk:$(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 (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": "okr-desk:" + 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": `okr-desk:${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" 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", "okr-desk:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<24) // the final message carries the whole reply
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", "okr-desk:" + M.readTree(rev).path("task").asText() + ":" + 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" => "okr-desk:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, 443, 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");
$task = json_decode($rev, true)["task"];
$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: okr-desk:$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", $"okr-desk:{JsonNode.Parse(rev)!["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);