Drive Operating Rhythm from your own code
The one metered task, rhythm, is available over HTTP. It takes a company's headcount and its recurring meetings (each with its level, how often and when it meets, its length and people, whether it decides, whether it is mostly status, whether it has a named decider), plus any slow decisions, together with the page's own calls, and returns the fix and a timed agenda for every meeting, a proposal for every missing level, a type and decider for every slow decision, the first move and when to look again.
The division of labour first: the page decides; the model writes the changes. The page applies the five-level meeting architecture: a call per meeting (keep, tighten, redesign, async, drop), the length and people each meeting should have after its call, the levels the headcount needs but lacks, the hours, the clashes and a verdict. It sends all of that as facts. The model never changes a call, a length, a headcount, a number or the verdict.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"operating-rhythm"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, company, meetings, decisions, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the reasons 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, the input and the verdicts
Every run sends task: rhythm (the one task is rhythm); a missing or different one is still answered as rhythm. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always rhythm. |
company | yes | The headcount and its size band, for example 85 people (30 to 100 people). |
meetings | yes | The meeting names in list order, separated by ; . |
decisions | yes | The slow decisions, separated by ; , or none. |
facts | yes | The page's lines: the verdict and why, the company line (band and the levels it needs), the load line (person-hours a week, your hours before and after, hours back), an optional cost line, one line per meeting (m1 to m12: name, level, when, length, people, how it decides, call, why, length and people after the call, hours a week and back, and any cadence note), one Gap: line per missing level, any Clash: lines, and one Slow decision d1: line per decision. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
Size bands: under 30 people needs weekly functional reviews and a weekly all-hands; 30 to 100 needs all five levels (daily standups, weekly functional reviews, a weekly all-hands, leadership alignment every two weeks, quarterly planning); 100 and up adds monthly skip-level reviews. Calls, first match wins: drop (a standup or leadership meeting under 30 people, a skip-level under 100); async (rarely decides and mostly status); redesign (rarely decides, or decides only sometimes and mostly status); tighten (over its level's length or people mark, no named decider, or decides only sometimes); keep. The verdict, first match wins: overloaded (more than 10 of your hours a week, or at least half the meetings async, redesign or drop); needs_work (any call other than keep, or a missing level); sound.
The reply
One JSON object, no prose around it:
{"task": "rhythm", "verdict": "overloaded", "headline": "...", "meetings": [{"id": "m1", "call": "async", "fix": "...", "agenda": []}, {"id": "m2", "call": "redesign", "fix": "...", "agenda": [{"min": 45, "item": "..."}, ...]}, ...], "gaps": [{"level": "leadership", "add": "..."}], "decisions": [{"id": "d1", "type": "type1", "decider": "...", "why": "..."}], "first_move": "...", "checkpoint": "..."}
| Field | What it holds |
|---|---|
verdict | The verdict from facts, copied exactly. |
headline | At most 28 words: the verdict in plain words with the person-hours a week and the hours that come back. |
meetings[] | One per meeting in list order, with id and call copied from facts, fix (what to change, built on the call's reasons) and agenda (for keep, tighten and redesign: 3 to 5 timed items whose minutes add up exactly to the length after the call; empty for async and drop). |
gaps[] | One per missing level, with level copied and add: how to start it. |
decisions[] | One per slow decision, with id, type (type1 hard to reverse, decided in one leadership meeting within 1-2 weeks; type2 easy to reverse, the owner decides the same day or the next), decider (a role) and why. |
first_move | The one change to make first. |
checkpoint | When to look again. |
The page then checks the reply (RhythmKit.check) - the verdict and every call against its own, one fix for every meeting and none for meetings not on the list, every agenda adding up to its meeting's length, an agenda only for meetings that still meet, a proposal for every missing level, a decider for every slow decision, every clash mentioned, every number in prose one the page gave, no internal ids, no personal names, nothing described as already done - and lists anything it finds under "The page's check".
Worked examples - every verdict and every call
These are the 4 free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover every verdict and every call.
Seed team, 18 people - needs work (drop, keep, tighten)
Input:
{
"task": "rhythm",
"company": "18 people (under 30 people)",
"meetings": "Morning standup; Monday all-hands; Product review; Founders sync; GTM pipeline review",
"decisions": "Which pricing tier to launch with; Whether to hire a second designer",
"facts": "Verdict: needs_work - 3 meetings need a change.\nCompany: 18 people, in the band under 30 people. This band needs weekly functional reviews and a weekly all-hands.\nLoad: 5 meetings take 58.5 person-hours a week. You sit in 5.4 hours a week; after the calls, 2.8 hours. Taking every call gives back 33 person-hours a week.\nm1 | Morning standup | daily standup | every weekday at 09:30, 20 min, 18 people | decisions: sometimes; mostly status updates; no named decider; you attend | call: drop | why: a company under 30 people does not need daily standups yet | length after the call: none | people after the call: none | 30 person-hours a week, 30 back\nm2 | Monday all-hands | all-hands | weekly on Monday at 10:00, 45 min, 18 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 45 min | people after the call: 18 | 13.5 person-hours a week, 0 back\nm3 | Product review | weekly functional review | weekly on Friday at 16:00, 60 min, 7 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 60 min | people after the call: 7 | 7 person-hours a week, 0 back\nm4 | Founders sync | leadership alignment | weekly on Tuesday at 11:00, 60 min, 3 people | decisions: most times; a named decider; you attend | call: drop | why: a company under 30 people does not need leadership alignment every two weeks yet | length after the call: none | people after the call: none | 3 person-hours a week, 3 back\nm5 | GTM pipeline review | weekly functional review | weekly on Tuesday at 16:00, 60 min, 5 people | decisions: most times; no named decider; you attend | call: tighten | why: it has no named decider | length after the call: 60 min | people after the call: 5 | 5 person-hours a week, 0 back\nGaps: none.\nSlow decision d1: Which pricing tier to launch with\nSlow decision d2: Whether to hire a second designer"
}
Reply:
{
"task": "rhythm",
"verdict": "needs_work",
"headline": "The rhythm needs work because 3 meetings need a change, it takes 58.5 person-hours a week, and taking every call gives back 33 person-hours a week.",
"meetings": [
{
"id": "m1",
"call": "drop",
"fix": "Stop Morning standup: the company is under 30 people, so daily standups are not needed yet, and a short written update on what finished, what starts, and what blocks can take its place.",
"agenda": []
},
{
"id": "m2",
"call": "keep",
"fix": "Keep Monday all-hands as it is: it already makes decisions with a named decider at the right length and size, so keep protecting real questions and answers at the end of each one.",
"agenda": [
{
"min": 10,
"item": "CEO update on the north star"
},
{
"min": 10,
"item": "Metric dashboard review"
},
{
"min": 15,
"item": "One deep dive discussion"
},
{
"min": 10,
"item": "Questions and answers"
}
]
},
{
"id": "m3",
"call": "keep",
"fix": "Keep Product review as it is: it already makes decisions with a named decider at the right length and size, so keep picking only one or two problems for the deep dive and sending the rest to follow-ups.",
"agenda": [
{
"min": 10,
"item": "Metric recap"
},
{
"min": 15,
"item": "Wins and blockers"
},
{
"min": 20,
"item": "One deep dive"
},
{
"min": 15,
"item": "Next week's priorities"
}
]
},
{
"id": "m4",
"call": "drop",
"fix": "Stop Founders sync: the company is under 30 people and does not need leadership alignment every two weeks yet; a short written update can cover its job instead.",
"agenda": []
},
{
"id": "m5",
"call": "tighten",
"fix": "Tighten GTM pipeline review by naming one decider by role, since it currently has none, while keeping its length and its people as they are.",
"agenda": [
{
"min": 10,
"item": "Metric recap"
},
{
"min": 15,
"item": "Wins and blockers"
},
{
"min": 20,
"item": "One deep dive"
},
{
"min": 15,
"item": "Next week's priorities"
}
]
}
],
"gaps": [],
"decisions": [
{
"id": "d1",
"type": "type1",
"decider": "the GTM lead",
"why": "Choosing a pricing tier to launch with is hard to reverse and carries real stakes, so it needs a short written proposal and a leadership meeting to debate and decide."
},
{
"id": "d2",
"type": "type2",
"decider": "the design lead",
"why": "Hiring a second designer is a single hire and easy to reverse, so the design lead decides, tells the people it affects, and adjusts later."
}
],
"first_move": "Start by dropping Morning standup, which gives back 30 person-hours a week, more than any other change on the list.",
"checkpoint": "Run this again after a month of the new rhythm to compare the person-hours, or sooner if headcount reaches 30 people."
}
Series B, 85 people, meeting-heavy - overloaded (make it async, redesign, tighten, keep)
Input:
{
"task": "rhythm",
"company": "85 people (30 to 100 people)",
"meetings": "Daily company standup; Weekly leadership meeting; Engineering review; Friday all-hands; Pipeline sync; Quarterly offsite",
"decisions": "Whether to enter the German market; Which bugs block the release; Approving a conference sponsorship",
"facts": "Verdict: overloaded - 3 of 6 meetings fail the decisions test or are not needed yet.\nCompany: 85 people, in the band 30 to 100 people. This band needs daily standups, weekly functional reviews, a weekly all-hands, leadership alignment every two weeks and quarterly planning.\nLoad: 6 meetings take 347.5 person-hours a week. You sit in 6.9 hours a week; after the calls, 3.9 hours. Taking every call gives back 227.5 person-hours a week.\nCost at $90 a person-hour: $31,275 a week, $1,626,300 a year; the calls give back $20,475 a week, $1,064,700 a year.\nm1 | Daily company standup | daily standup | every weekday at 09:00, 30 min, 85 people | decisions: rarely or never; mostly status updates; no named decider; you attend | call: async | why: it makes no decisions and is mostly status updates | length after the call: none | people after the call: none | 212.5 person-hours a week, 212.5 back\nm2 | Weekly leadership meeting | leadership alignment | weekly on Monday at 13:00, 120 min, 9 people | decisions: sometimes; mostly status updates; a named decider; you attend | call: redesign | why: it decides only sometimes and is mostly status updates; it runs 120 min, over the 90-min mark for this level | length after the call: 90 min | people after the call: 9 | 18 person-hours a week, 4.5 back | the architecture runs this level every two weeks\nm3 | Engineering review | weekly functional review | weekly on Wednesday at 16:00, 75 min, 14 people | decisions: most times; a named decider | call: tighten | why: it runs 75 min, over the 60-min mark for this level; it holds 14 people, over the 10-person mark | length after the call: 60 min | people after the call: 10 | 17.5 person-hours a week, 7.5 back\nm4 | Friday all-hands | all-hands | weekly on Friday at 15:00, 60 min, 85 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 60 min | people after the call: 85 | 85 person-hours a week, 0 back\nm5 | Pipeline sync | other recurring meeting | weekly on Tuesday at 11:00, 45 min, 12 people | decisions: rarely or never; no named decider; you attend | call: redesign | why: it rarely or never makes a decision; it holds 12 people, over the 8-person mark; it has no named decider | length after the call: 45 min | people after the call: 8 | 9 person-hours a week, 3 back\nm6 | Quarterly offsite | quarterly planning | quarterly, 480 min, 9 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 480 min | people after the call: 9 | 5.5 person-hours a week, 0 back\nGaps: none.\nSlow decision d1: Whether to enter the German market\nSlow decision d2: Which bugs block the release\nSlow decision d3: Approving a conference sponsorship"
}
Reply:
{
"task": "rhythm",
"verdict": "overloaded",
"headline": "Overloaded: meetings take 347.5 person-hours a week since 3 of 6 fail the decisions test or are not needed yet; the calls give back 227.5 person-hours a week.",
"meetings": [
{
"id": "m1",
"call": "async",
"fix": "A daily written update replacing it each weekday covers what finished, what starts and what blocks; each person posts it themselves, and questions go to the team's usual channel.",
"agenda": []
},
{
"id": "m2",
"call": "redesign",
"fix": "Making this a decision meeting means the decisions awaiting resolution open the agenda, status updates move to a written note read in advance, and it runs 90 minutes with the same 9 people; weigh the two-week cadence this level normally runs on against meeting weekly.",
"agenda": [
{
"min": 35,
"item": "Decisions that need resolving"
},
{
"min": 15,
"item": "North star progress update"
},
{
"min": 25,
"item": "Short functional updates from each team"
},
{
"min": 15,
"item": "Priorities for the next two weeks"
}
]
},
{
"id": "m3",
"call": "tighten",
"fix": "Cutting the engineering review to 60 minutes and down to 10 people keeps the decider and whoever owns an item in the room, while everyone else reads the notes afterward.",
"agenda": [
{
"min": 10,
"item": "Metric recap"
},
{
"min": 15,
"item": "Wins and blockers"
},
{
"min": 25,
"item": "One deep dive"
},
{
"min": 10,
"item": "Next week's priorities"
}
]
},
{
"id": "m4",
"call": "keep",
"fix": "The Friday all-hands works because it makes decisions with a named decider at the right length and size; protect the real questions and answers at the end so it stays a discussion, not a broadcast.",
"agenda": [
{
"min": 10,
"item": "CEO update on the north star"
},
{
"min": 15,
"item": "Metric dashboard review"
},
{
"min": 25,
"item": "One deep dive discussion"
},
{
"min": 10,
"item": "Questions and answers"
}
]
},
{
"id": "m5",
"call": "redesign",
"fix": "Turning the pipeline sync into a decision meeting means opening with whatever decisions it exists to make, moving any status into a short written note beforehand, naming one decider by role, and running it at 45 minutes with 8 people.",
"agenda": [
{
"min": 15,
"item": "Decision this meeting exists to make"
},
{
"min": 20,
"item": "Items feeding that decision"
},
{
"min": 10,
"item": "Owners and next steps"
}
]
},
{
"id": "m6",
"call": "keep",
"fix": "The quarterly offsite works because it makes decisions with a named decider at the right length and size; protect the habit of giving each priority an owner, a metric and naming what the company stops doing.",
"agenda": [
{
"min": 60,
"item": "Retrospective on last quarter's priorities"
},
{
"min": 150,
"item": "Set next quarter's priorities and owners"
},
{
"min": 210,
"item": "Function breakout sessions"
},
{
"min": 60,
"item": "Synthesis across functions"
}
]
}
],
"gaps": [],
"decisions": [
{
"id": "d1",
"type": "type1",
"decider": "the leadership team",
"why": "Entering the German market is large and hard to reverse, so it should move through a short written proposal and get decided in one leadership meeting."
},
{
"id": "d2",
"type": "type2",
"decider": "the engineering lead",
"why": "Which bugs block the release is easy to reverse and routine, so the owner can decide, tell whoever is affected, and adjust it again later."
},
{
"id": "d3",
"type": "type2",
"decider": "the budget owner",
"why": "Approving a conference sponsorship is a small, easy to reverse approval, so the owner can decide, tell whoever is affected, and adjust it later."
}
],
"first_move": "Make the daily company standup the first move, since replacing it with a written update gives back 212.5 person-hours a week, more than any other meeting.",
"checkpoint": "Run this again after a month of the new rhythm to compare the person-hours, or sooner if headcount reaches 100 people, the start of the next band."
}
Scale-up, 220 people (€) - sound (keep)
Input:
{
"task": "rhythm",
"company": "220 people (100 to 300 people)",
"meetings": "Team standups; Product weekly; GTM weekly; All-hands; Leadership alignment; Skip-levels; Quarterly planning",
"decisions": "none",
"facts": "Verdict: sound - every meeting makes decisions at the right length and size, and every level this size needs is there.\nCompany: 220 people, in the band 100 to 300 people. This band needs daily standups, weekly functional reviews, a weekly all-hands, leadership alignment every two weeks, quarterly planning and monthly skip-level reviews.\nLoad: 7 meetings take 262 person-hours a week. You sit in 4.6 hours a week; after the calls, 4.6 hours. Taking every call gives back 0 person-hours a week.\nCost at €70 a person-hour: €18,340 a week, €953,680 a year; the calls give back €0 a week, €0 a year.\nm1 | Team standups | daily standup | every weekday at 09:15, 15 min, 8 people | decisions: most times; a named decider | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 15 min | people after the call: 8 | 10 person-hours a week, 0 back\nm2 | Product weekly | weekly functional review | weekly on Friday at 16:00, 60 min, 8 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 60 min | people after the call: 8 | 8 person-hours a week, 0 back\nm3 | GTM weekly | weekly functional review | weekly on Tuesday at 16:00, 60 min, 9 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 60 min | people after the call: 9 | 9 person-hours a week, 0 back\nm4 | All-hands | all-hands | weekly on Thursday at 17:00, 60 min, 220 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 60 min | people after the call: 220 | 220 person-hours a week, 0 back\nm5 | Leadership alignment | leadership alignment | every two weeks on Monday at 14:00, 90 min, 8 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 90 min | people after the call: 8 | 6 person-hours a week, 0 back\nm6 | Skip-levels | skip-level review | monthly on Wednesday at 11:00, 60 min, 12 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 60 min | people after the call: 12 | 2.8 person-hours a week, 0 back\nm7 | Quarterly planning | quarterly planning | quarterly, 480 min, 10 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 480 min | people after the call: 10 | 6.2 person-hours a week, 0 back\nGaps: none.\nSlow decisions: none listed."
}
Reply:
{
"task": "rhythm",
"verdict": "sound",
"headline": "The rhythm is sound, holding steady at 262 person-hours a week with 0 person-hours a week due back, so no changes are needed.",
"meetings": [
{
"id": "m1",
"call": "keep",
"fix": "Team standups work because they stay short and focused on what finished, what starts and what blocks, with a named decider keeping it moving. Protect that: never let it drift into status updates or strategy discussion.",
"agenda": [
{
"min": 5,
"item": "What finished since yesterday"
},
{
"min": 5,
"item": "What starts today"
},
{
"min": 5,
"item": "What blocks progress"
}
]
},
{
"id": "m2",
"call": "keep",
"fix": "Product weekly works because it brings the function's leaders through a metric recap, wins and blockers, and one deep dive before setting next week's priorities. Protect the habit of narrowing to a couple of problems and sending the rest to follow-up discussion.",
"agenda": [
{
"min": 10,
"item": "Metric recap"
},
{
"min": 15,
"item": "Wins and blockers"
},
{
"min": 25,
"item": "One deep dive"
},
{
"min": 10,
"item": "Next week's priorities"
}
]
},
{
"id": "m3",
"call": "keep",
"fix": "GTM weekly works because the function's leaders work through a metric recap, wins and blockers, then one deep dive, before agreeing next week's priorities. Protect the habit of narrowing to a couple of problems and routing the rest to follow-up.",
"agenda": [
{
"min": 10,
"item": "Metric recap"
},
{
"min": 15,
"item": "Wins and blockers"
},
{
"min": 25,
"item": "One deep dive"
},
{
"min": 10,
"item": "Next week's priorities"
}
]
},
{
"id": "m4",
"call": "keep",
"fix": "All-hands works because everyone sees the same metric dashboard every week alongside a north star update, one deep dive discussion, and real questions and answers. Protect that same dashboard and real question time rather than letting it become one-way.",
"agenda": [
{
"min": 10,
"item": "North star update"
},
{
"min": 10,
"item": "Metric dashboard"
},
{
"min": 25,
"item": "One deep dive discussion"
},
{
"min": 15,
"item": "Questions and answers"
}
]
},
{
"id": "m5",
"call": "keep",
"fix": "Leadership alignment works because it moves through north star progress, short functional updates and the major decisions that need resolving, before looking at the next two weeks. Protect that focus on decisions that need resolving, not status updates.",
"agenda": [
{
"min": 15,
"item": "North star progress"
},
{
"min": 20,
"item": "Short functional updates"
},
{
"min": 40,
"item": "Decisions that need resolving"
},
{
"min": 15,
"item": "Next two weeks"
}
]
},
{
"id": "m6",
"call": "keep",
"fix": "Skip-levels work because a leader meets with people two levels down to hear what no longer reaches them through the normal chain. Protect that direct, monthly listening habit rather than letting it turn into status reporting.",
"agenda": [
{
"min": 10,
"item": "Welcome and context"
},
{
"min": 30,
"item": "What isn't reaching you"
},
{
"min": 15,
"item": "Specific examples and blockers"
},
{
"min": 5,
"item": "Next steps"
}
]
},
{
"id": "m7",
"call": "keep",
"fix": "Quarterly planning works because it moves from a retrospective of the last quarter to next quarter's priorities, each with an owner and a metric, then function breakouts and synthesis. Protect naming an owner and a metric for every priority.",
"agenda": [
{
"min": 60,
"item": "Retrospective of the last quarter"
},
{
"min": 180,
"item": "Next quarter's priorities"
},
{
"min": 180,
"item": "Function breakouts"
},
{
"min": 60,
"item": "Synthesis"
}
]
}
],
"gaps": [],
"decisions": [],
"first_move": "Keep protecting the habit already in place everywhere: every meeting keeps a named decider who decides most of the time, so the first move is simply holding that pattern steady across every meeting you listed.",
"checkpoint": "Run this again after a month of the new rhythm to compare the person-hours, or sooner if headcount reaches 300."
}
Remote company, 60 people (£) - needs work (keep, tighten, redesign)
Input:
{
"task": "rhythm",
"company": "60 people (30 to 100 people)",
"meetings": "Team standup; Product and eng review; Town hall; Customer escalations; Marketing check-in",
"decisions": "Who approves a refund over £500; Whether to sunset the legacy plan",
"facts": "Verdict: needs_work - 2 meetings need a change and 2 levels are missing for this size.\nCompany: 60 people, in the band 30 to 100 people. This band needs daily standups, weekly functional reviews, a weekly all-hands, leadership alignment every two weeks and quarterly planning.\nLoad: 5 meetings take 36.1 person-hours a week. You sit in 1.7 hours a week; after the calls, 1.7 hours. Taking every call gives back 0 person-hours a week.\nCost at £65 a person-hour: £2,347 a week, £122,044 a year; the calls give back £0 a week, £0 a year.\nm1 | Team standup | daily standup | every weekday at 10:00, 15 min, 7 people | decisions: most times; a named decider | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 15 min | people after the call: 7 | 8.8 person-hours a week, 0 back\nm2 | Product and eng review | weekly functional review | weekly on Wednesday at 15:00, 60 min, 8 people | decisions: sometimes; a named decider; you attend | call: tighten | why: it decides only sometimes | length after the call: 60 min | people after the call: 8 | 8 person-hours a week, 0 back\nm3 | Town hall | all-hands | monthly on Thursday at 16:00, 60 min, 60 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 60 min | people after the call: 60 | 13.8 person-hours a week, 0 back | the architecture runs this level weekly\nm4 | Customer escalations | other recurring meeting | weekly on Wednesday at 15:30, 30 min, 6 people | decisions: most times; a named decider; you attend | call: keep | why: it makes decisions with a named decider at the right length and size | length after the call: 30 min | people after the call: 6 | 3 person-hours a week, 0 back\nm5 | Marketing check-in | other recurring meeting | weekly on Monday at 11:00, 30 min, 5 people | decisions: rarely or never; a named decider | call: redesign | why: it rarely or never makes a decision | length after the call: 30 min | people after the call: 5 | 2.5 person-hours a week, 0 back\nGap: leadership | Leadership alignment | every two weeks | 90 min\nGap: quarterly | Quarterly planning | quarterly | 480 min\nClash: Product and eng review and Customer escalations overlap on Wednesday.\nSlow decision d1: Who approves a refund over £500\nSlow decision d2: Whether to sunset the legacy plan"
}
Reply:
{
"task": "rhythm",
"verdict": "needs_work",
"headline": "The rhythm needs work, with 2 meetings needing a change and 2 levels missing for this size, costing 36.1 person-hours a week and giving back 0.",
"meetings": [
{
"id": "m1",
"call": "keep",
"fix": "Team standup works because it stays short with a named decider making real calls. Protect the habit of sticking to what finished, what starts and what blocks, without status reports or strategy talk.",
"agenda": [
{
"min": 5,
"item": "What finished since yesterday"
},
{
"min": 5,
"item": "What starts today"
},
{
"min": 5,
"item": "What could block progress"
}
]
},
{
"id": "m2",
"call": "tighten",
"fix": "Product and eng review should open with the decision it needs to make that week, since it only decides sometimes, with the named decider still making the call. It also overlaps Customer escalations on Wednesday, so one of the two should move.",
"agenda": [
{
"min": 15,
"item": "The decision to resolve this week"
},
{
"min": 10,
"item": "Metric recap"
},
{
"min": 10,
"item": "Wins and blockers"
},
{
"min": 15,
"item": "One deep dive"
},
{
"min": 10,
"item": "Next week's priorities"
}
]
},
{
"id": "m3",
"call": "keep",
"fix": "Town hall works because it makes real decisions at the right length for everyone, with questions and answers kept genuine. Weigh that this level is usually run weekly, not monthly, under the architecture.",
"agenda": [
{
"min": 15,
"item": "CEO update on the north star"
},
{
"min": 15,
"item": "The metric dashboard"
},
{
"min": 15,
"item": "One deep dive discussion"
},
{
"min": 15,
"item": "Questions and answers"
}
]
},
{
"id": "m4",
"call": "keep",
"fix": "Customer escalations works because it opens with the decision and keeps a named decider at a workable size. Protect the habit of moving straight from the decision to owners and next steps.",
"agenda": [
{
"min": 10,
"item": "The decision this meeting exists for"
},
{
"min": 10,
"item": "Items feeding that decision"
},
{
"min": 10,
"item": "Owners and next steps"
}
]
},
{
"id": "m5",
"call": "redesign",
"fix": "Marketing check-in should turn into a decision meeting, opening with the decision it needs to make rather than status updates. Move routine updates into a short written update read beforehand, keeping it at its length and people after the call.",
"agenda": [
{
"min": 10,
"item": "The decision to make this week"
},
{
"min": 10,
"item": "Items feeding that decision"
},
{
"min": 10,
"item": "Owners and next steps"
}
]
}
],
"gaps": [
{
"level": "leadership",
"add": "Add leadership alignment every two weeks for 90 minutes, with the leadership team covering north star progress, short functional updates, and the major decisions pending, building on the north star update Town hall already gives."
},
{
"level": "quarterly",
"add": "Add quarterly planning once a quarter for 480 minutes, with the leadership team and function leads retrospecting on the last quarter, setting next quarter's priorities with an owner and metric each, and running function breakouts into a synthesis."
}
],
"decisions": [
{
"id": "d1",
"type": "type2",
"decider": "the customer escalations lead",
"why": "This is easy to reverse, a routine approval, so the customer escalations lead decides, tells the people affected and adjusts later if needed."
},
{
"id": "d2",
"type": "type1",
"decider": "the product lead",
"why": "This is hard to reverse and carries high stakes, so it needs a short written proposal first, then debate and a decision at a leadership meeting."
}
],
"first_move": "Start by adding leadership alignment every two weeks, since it is the first missing level for a company this size and no meeting change gives back any hours.",
"checkpoint": "Run this again after a month of the new rhythm to compare the person-hours, or sooner if headcount reaches 100."
}
Build the input yourself
Load the engine in node (global.window = global; require("./rhythmkit.js")), then:
const K = RhythmKit;
const form = K.form({ cur: "$", headcount: "85", rate: "90", slow: "Whether to enter the German market", meetings: [
{ name: "Daily company standup", kind: "standup", often: "daily", day: "mon", at: "09:00", minutes: "30", people: "85", decides: "no", status: true, decider: false, mine: true },
{ name: "Friday all-hands", kind: "allhands", often: "weekly", day: "fri", at: "15:00", minutes: "60", people: "85", decides: "yes", status: false, decider: true, mine: true }] });
const built = K.buildInput(form); // built.facts holds each meeting's call, why, length and people after, hours, gaps, verdict
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, K.title(form)) // the rhythm as Markdown; K.ics(built, reply) gives the new rhythm as a calendar
Form keys: cur, headcount, rate (cost of a person-hour, optional), slow (slow decisions, one per line, up to 6) and up to twelve meetings, each with name, kind (standup, functional, allhands, leadership, quarterly, skiplevel, other), often (daily, weekly, biweekly, monthly, quarterly), day (mon to fri), at (HH:MM), minutes, people, decides (yes, sometimes, no), status, decider and mine.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://operating-rhythm.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://operating-rhythm.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"]
// operating-rhythm.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://operating-rhythm.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://operating-rhythm.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
}
// OperatingRhythm.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 OperatingRhythm {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://operating-rhythm.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://operating-rhythm.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://operating-rhythm.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://operating-rhythm.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":"operating-rhythm"}' | 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": "operating-rhythm"})["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: "operating-rhythm" })).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": "operating-rhythm"}, 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", "operating-rhythm"), 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: "operating-rhythm" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "operating-rhythm"])["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 = "operating-rhythm" }))!["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": "rhythm", "company": "85 people (30 to 100 people)", "meetings": "...", "decisions": "...", "facts": "...", ...}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "rhythm", "company": "85 people (30 to 100 people)", "meetings": "...", "decisions": "...", "facts": "...", ...}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "rhythm", company, meetings, decisions, facts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses operating-rhythm:<task>:<inputHash>:a<attempt> (the one task is rhythm), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="operating-rhythm:rhythm:$(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}'
import hashlib, time
key = "operating-rhythm:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `operating-rhythm:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "operating-rhythm:" + 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 }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "operating-rhythm:" + 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);
require "digest"
key = "operating-rhythm:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
$key = "operating-rhythm:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"operating-rhythm:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
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: operating-rhythm:rhythm:$(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": "rhythm", "company": "85 people (30 to 100 people)", "meetings": "...", "decisions": "...", "facts": "...", ...}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "operating-rhythm:rhythm:" + 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": `operating-rhythm:rhythm:${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", "operating-rhythm:rhythm:"+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", "operating-rhythm:rhythm:" + 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" => "operating-rhythm:rhythm:#{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: operating-rhythm:rhythm:" . 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", $"operating-rhythm:rhythm:{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);