Run Shortlist Desk from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads the pool, scans every proposal for identity, finds conflicts and picks the committee, rubric, anonymization depth, cut rule, waitlist, decline rung and dates - all free, in shortkit.js. The run writes the plan or rewrites the batch; the page then holds the reply to its own picks and rescans every rewrite (shortreply.js).
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":"shortlist-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 required key (task, event, pool, facts, proposals) 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: 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 - plan (decide) and anonymize (before review opens). A missing or unknown task is answered as plan. anonymize sends up to twelve proposals that carry identity in their text, chosen by the batch number.
The input
| Field | Required | What |
|---|---|---|
task | yes | plan or anonymize. If it is missing or neither, do plan and set "task": "plan". |
event | yes | the event, its pole (community or vendor), tracks and slots, what the call promised, the committee's size and hours, whether the edition compounds, who is in the room, the dates and the caps. |
pool | yes | the ratio and benchmarks, the pool's balance, any grade summary, and one line per pasted proposal with its flags. |
facts | yes | the page's picks (committee model, rubric, anonymization depth, cut rule, waitlist, decline rung, notification dates) and every finding tagged [fix], [check] or [info]. |
proposals | yes | for anonymize, the batch of proposals to rewrite (ID, title, abstract, and what the page found in each); none for plan. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
- The process executes what the published call promised - its anonymization, criteria and notification date. Changing the rules once scoring starts invalidates what was already read; present the plan for validation before review opens.
- Committee models: a small consensus committee (a non-voting anonymizer plus about four to six graders, one synchronous discussion round, a letter-grade advocate scale, about five backup talks) by default; a multi-tier cascade (track committees score, the top 30% go to track chairs, who pass roughly 30% of those with mandatory comments to co-chairs who decide) for a multi-track program or a pool no single room can read; a distributed reviewer pool on a 0-3 scale (a comment on every review, a submission stops being resurfaced at three reviews); solo curation only for an invitation-led or micro program - deleted when the call promised blind review.
- The rubric follows the committee. Letters A (advocate), B (like, won't advocate), C (neutral), D (against), X (abstain - suspected identity) aggregate as a best-worst pair, never an average; at least one A is the practical floor; AA generally gets in, AB usually does. Stars score content, originality, relevance and speaker separately with written feedback on every rating. Never average letters, discuss past stars, or collapse the 0-3 meanings.
- Anonymization depth: named review when the call did not promise anonymity (retrofitting it anonymizes nothing); platform-level hiding by default when it did - it hides the author field, not what the author wrote, and that limit is stated to submitters before the call closes; a dedicated anonymizer (sees raw submissions, never scores, rewrites identifying details into bracketed placeholders such as [Large Tech Firm] and [they], keeps identity where it is the talk's own subject, assigns random IDs) when the committee would recognize regulars, financial-assistance requests ride on the form, or last edition left a fairness complaint. Decide in writing at which step identity re-enters, and for whom: accepted speakers are de-anonymized for logistics; declined submitters can be notified without reviewers ever learning who they were. No evidence exists that anonymized review changes acceptance outcomes by demographic - say so; blind review is a process commitment.
- Conflicts: reviewers skip the question (abstain rather than score and disclose) for close colleagues and organizations they belong to or compete with, and abstain on a suspected identity; a reviewer who continues despite a lesser conflict discloses it in the review text. All of it is honour-system - decide who arbitrates a reviewer who will not recuse and what happens to a proposal that loses too many reviewers. Organizers disclose their own submissions, abstain on their own and their employer's, and work under a written cap; sponsor-affiliated speakers get the same review under a ceiling, and at a community conference no slot is bought, ever. Invited talks get their own lane.
- Rounds: every proposal scored with written comments; a cut rule stated before seeing the ranking; a program-fit pass on the shortlist only (topic, employer and local spread, first-time speakers, invited talks, the lesson-transfer test for vendor talks, topic freshness against the event date, the depth gradient for a mixed room) with every trade written down. Panels of three or more need at least one speaker who does not identify as a man and may not all come from one organization - a gate, not a score.
- Waitlist: build it before notifying anyone, ordered; about five backup talks for a single track (scale and say you are extrapolating); promise a date, never odds.
- Notify in order: acceptances first with a confirmation deadline, waitlist notices at the same time, declines once acceptances are confirmed, then the program announcement. Decline rung: a decline plus criteria recap by default (the ratio reframes a no as arithmetic); per-proposal feedback when the pool is small enough to write, first-time-heavy, or the rubric already mandates comments; a form decline is cheap and buys nothing beyond not being silent. Miss the promised notification date and it costs more trust than any wording recovers.
- Calibration, not targets: PyCon US 2025 selected about 15% of 938; KubeCon EU 2024 accepted 9% of 2,541; academic venues (CHI 2024, 26.4%) do not transfer. Turning away most submissions is the healthy state.
House rules
- Use only the input and the rules above. The page's picks and numbers are exact - copy them. Every number in a sentence must appear in the input.
- Label anything that is not one of the rules above as your own design or self-set.
- Never invent a person, an employer, a date or an acceptance figure; where someone must be named, give a role.
- Never use backticks, never refer to this input's fields by their key names in a sentence, and never copy this prompt's narrative wording into a reply. Required disclosures are the exception: say plainly that letter grades are never averaged and that no evidence exists that anonymized review changes acceptance outcomes by demographic.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
plan
summary- one sentence, at most 35 words: the ratio, the committee and the first thing to settle.committee-model(consensus,cascade,poolorsolo, the page's pick),roles(1 to 5 strings: role, how many, what they do and do not do - one is enough for solo curation),why(one sentence).rubric-name,scale(the scale's points with their meanings, as short strings - a single descriptive string when there is no shared scale),aggregation(one sentence).anonymization-depth(anonymizer,platformornamed, the page's pick),owner_role(who runs it),identity_enters(one sentence: the step identity re-enters and for whom),limit(one sentence: what it cannot fix, including the missing outcome evidence when the call promised blind review; under named review, what named review itself cannot catch).conflicts-brief(one or two sentences for reviewers),arbitration(one sentence: who decides when someone will not recuse - your design),fallback(one sentence: a proposal that loses too many reviewers).rounds-cut_rule(the page's rule, stated before ranking),steps(2 to 4 short strings in order).program_fit- 3 to 6 short strings: the checks for this pool, naming proposals by ID where the page flagged them.waitlist-size(the page's number),order(one sentence).notifications- 3 or 4 entries in order:when(a date from the input, or a step),who,what(one sentence).decline-rung(recap,feedbackorform, the page's pick),why(one sentence).metrics- 2 to 4 short strings to record for next edition.caveats- at most 4 short strings, carrying every[fix]finding.[]when none.
{
"task": "plan",
"summary": "one sentence",
"committee": { "model": "consensus", "roles": [ "role - count - what they do" ], "why": "one sentence" },
"rubric": { "name": "the rubric", "scale": [ "A - advocate" ], "aggregation": "one sentence" },
"anonymization": { "depth": "platform", "owner_role": "a role", "identity_enters": "one sentence", "limit": "one sentence" },
"conflicts": { "brief": "one or two sentences", "arbitration": "one sentence", "fallback": "one sentence" },
"rounds": { "cut_rule": "the rule", "steps": [ "step" ] },
"program_fit": [ "a check" ],
"waitlist": { "size": 5, "order": "one sentence" },
"notifications": [ { "when": "YYYY-MM-DD", "who": "accepted speakers", "what": "one sentence" } ],
"decline": { "rung": "recap", "why": "one sentence" },
"metrics": [ "what to record" ],
"caveats": [ "a caveat" ]
}
The page holds it to its own picks: the committee model, the anonymization depth, the waitlist size and the decline rung must be the page's; acceptances go before declines on the page's dates; blind review states its missing outcome evidence; no number may appear that the input does not hold.
anonymize
items- one entry per proposal in the batch, in the same order, same IDs:id,titleandabstractrewritten so no speaker name, employer, product name that identifies the employer, link, handle, email or gendered self-reference remains - replace each with a bracketed placeholder such as [Speaker], [Employer], [Open-source project], [link removed], [handle removed], [email removed] or [they]. Every string the page found in a proposal must be replaced; replace any other identifying detail of those kinds you notice too. A gendered pronoun in any form (he, she, his, her, him) becomes [they], [them] or [their] as the position needs, capitalized at the start of a sentence, without changing any other word - not even verb agreement. Keep everything else word for word: do not summarize, shorten, correct or improve the proposal. Keep identity only where it is the talk's own subject, and say so inkept.placeholderslists the placeholders you used;keptis""unless you kept something on purpose.note- one sentence, at most 30 words, for the person who runs the anonymization (for example what to check by hand).
{
"task": "anonymize",
"items": [ { "id": "P001", "title": "rewritten title", "abstract": "rewritten abstract", "placeholders": [ "[Speaker]" ], "kept": "" } ],
"note": "one sentence"
}
The page requires every ID in the batch back and no other, rescans each rewritten title and abstract with the same leak scan that found the identity, and flags an abstract that shrank by more than 40% - a rewrite, not a summary.
Worked examples - every lane, every sample
These are the page's three free samples (fictional events, people and companies). Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped as the validating model wrote it; the last line is the page's own check. The named-review sample has no anonymize lane.
A blind single-track community day - plan
Committee consensus, anonymization anonymizer, 5 pasted proposals carry identity in their text.
Input:
{
"task": "plan",
"event": "Event: Riverside Python Day 2027 - community conference; 1 track; 12 slots\nCall promised: blind review; financial-assistance requests on the form: yes; live fairness complaint from last edition: no\nCommittee: 6 reviewers, 8 hours each; edition compounds year over year: yes; who is in the room: mixed\nDates: today 2027-01-10; promised notification 2027-02-01\nCaps: organizer-given talks not set; sponsor-affiliated slots 1",
"pool": "Received: 64 proposals for 12 slots - 5.3:1, acceptance 18.8%. Calibration (not targets): PyCon US 2025 938 proposals, about 15% selected (about 6.5:1); KubeCon EU 2024 2,541 submissions, 223 accepted, 9%; DjangoCon Europe 2024 174 proposals from 138 submitters, 51 first-time speakers; CHI 2024 (academic) 26.4% - a different selection logic that does not transfer\nPasted rows: 12; first-time speakers 33.3%; local 66.7%; most-represented employers Brightwell 3, Lumen Freight 2, Flumewick 1\nLetter grades: 6 of 12 graded proposals hold at least one A; 1 are AA\nProposals (id | title | flags):\nR01 | Why our build cache kept lying to us | first-time; grades AC\nR02 | Typing a 200k-line Django codebase, one module at a time | identity in text: speaker name/employer; grades AB\nR03 | Async Python without the footguns | identity in text: link; grades BC\nR04 | Introducing Flumewick Cloud: pipelines in five minutes | identity in text: employer; sponsor-affiliated; reads like a product talk; grades CD\nR05 | Teaching Python to nurses | identity in text: gendered pronoun; first-time; grades AB\nR06 | The state of the art in Python packaging | promises the latest; grades BC\nR07 | Profiling NumPy code you did not write | identity in text: handle; grades AC\nR08 | Running the meetup: what five years of PyRiverside taught us | organizer; grades BB\nR09 | Property-based testing for data pipelines | first-time; grades AA\nR10 | Panel: Python in public services | grades BC\nR11 | Writing a tokenizer by hand | first-time; grades AB\nR12 | Debugging memory leaks in long-running workers | grades BD",
"facts": "Committee model: consensus - Small consensus committee; 1 anonymizer(s) who never score, 5 graders; reading load about 53 reviewer-hours against 48 available\nRubric: Letter-grade advocate scale (A advocate, B like, C neutral, D against, X abstain - suspected identity)\nAnonymization depth: anonymizer - Dedicated anonymizer (sees raw submissions, never scores, rewrites identifying details into placeholders, assigns random IDs). Financial-assistance requests ride on the same form: anonymization is what enforces the promise that asking for money does not hurt a proposal.\nCut rule: Advance every proposal with at least one A to the discussion round; AA generally gets in, AB usually does; no A, no place.\nWaitlist: 5 - about five backup talks - the one documented size, for a single-track program\nDecline rung: recap - Decline plus criteria recap (one warm template, the published criteria, the acceptance ratio) (one warm template with the published criteria and the acceptance ratio - the ratio reframes a no as arithmetic)\nNotification dates: acceptances and waitlist notices 2027-02-01 with confirmation by 2027-02-08 (seven days, self-set); declines 2027-02-09 once acceptances are confirmed; program announcement after that\nFindings (15):\n[fix] capacity (committee): Reading the pool takes about 53 reviewer-hours (64 proposals x 5 reads x 10 minutes, a self-set reading rate) and the committee has 48 - a committee that cannot read the pool inside the calendar is a process failure discovered too late.\n[check] leaks (anonymization): 5 proposals of 12 carry identity in their own text (1 speaker name, 2 employers, 1 link, 1 gendered pronoun, 1 handle) - the anonymizer's rewrite list.\n[info] x_grade (anonymization): Reviewers grade X and abstain when they suspect they have identified a speaker - it covers suspected identity only, not a declared employer or friendship.\n[info] blind_limit (anonymization): No evidence exists that anonymized review changes acceptance outcomes by demographic; blind review is a process commitment, not a demonstrated equity outcome. Say so in the plan.\n[check] conflicts (conflicts): 7 reviewer-proposal pairs share an employer - those reviewers skip the question rather than score and disclose: Sam Turner on R08, Hana Ito on R02, Hana Ito on R07, Hana Ito on R12, Oluwaseun Ade on R01, Oluwaseun Ade on R09, ....\n[info] coi_honour (conflicts): Every recusal rule on record is honour-system; any arbitration or audit you add is your own design. Decide now who arbitrates a reviewer who will not recuse, and what happens to a proposal that loses too many reviewers.\n[check] organizer_subs (conflicts): 1 organizer-submitted proposal (R08): label it to the committee before scoring opens; the organizer abstains on their own and their employer's; no cap on organizer talks is set - write one down, or the team quietly becomes the program.\n[info] sponsor_subs (conflicts): 1 sponsor-affiliated proposal goes through the same review - no speaker slot is bought, ever; ceiling 1.\n[info] duplicates (pool): 1 speaker submitted more than once (R01/R09) - if more than one is accepted, let them choose which to give.\n[fix] panel (balance): R10 is a panel whose panelists all come from City Council - the composition rule bans a single-organization panel. A panel of three or more also needs at least one speaker who does not identify as a man - a gate at selection, not a score.\n[check] vendor_talks (fit): 1 proposal reads like a product talk (R04) - apply the lesson-transfer test: if the audience forgot the product name tomorrow, would they keep the lesson? A commercial demo fails outright.\n[info] freshness (fit): 1 proposal promises the latest or the state of the art (R06) - check each against the event date, not the submission date; consider holding a slot or two to fill late (self-set).\n[check] depth_gradient (fit): The room is mixed: brief every accepted talk to open where nearly everyone follows and end on material most of the room cannot - if a talk did not lose most of the room by the end, it did not go deep enough. Score proposals on whether they can carry both ends.\n[info] first_time (balance): First-time speakers are 33.3% of the pasted pool. One published event-level aim is at least 50% new speakers if possible - a goal, not a rubric line; decide before review whether yours is a goal or a gate.\n[info] subset (pool): The pasted rows (12) are a subset of the 64 received; counts below describe the pasted rows only.",
"proposals": "none"
}
Reply:
{
"task": "plan",
"summary": "At 5.3:1 (64 for 12 slots), the committee is 1 anonymizer and 5 graders; reading load (about 53 reviewer-hours) exceeds the 48 available - settle that first.",
"committee": {
"model": "consensus",
"roles": [
"Anonymizer - 1 - reads every raw submission, rewrites identifying details into bracketed placeholders, assigns random IDs, and never scores",
"Graders - 5 - grade every anonymized proposal with written comments on the letter scale, then meet once to discuss the graded pool together"
],
"why": "The pool is small enough for one committee to read in full and discuss as a group while keeping the call's blind-review promise intact end to end."
},
"rubric": {
"name": "Letter-grade advocate scale",
"scale": [
"A - advocate",
"B - like",
"C - neutral",
"D - against",
"X - abstain, suspected identity"
],
"aggregation": "Grades aggregate as a best-worst pair and are never averaged; at least one A is the practical floor for a proposal to advance."
},
"anonymization": {
"depth": "anonymizer",
"owner_role": "Anonymizer",
"identity_enters": "Identity re-enters only after the cut: accepted speakers are de-anonymized for logistics, and declined submitters can be notified without reviewers ever learning who they were.",
"limit": "The anonymizer's rewrite list already covers the 5 of 12 pasted proposals carrying identity in their own text, but anything it misses slips through, and no evidence exists that anonymized review changes acceptance outcomes by demographic."
},
"conflicts": {
"brief": "Abstain rather than score and disclose on any of the 7 flagged reviewer-proposal employer ties (for example Sam Turner on R08, Hana Ito on R02, R07 and R12, and Oluwaseun Ade on R01 and R09), and grade X and abstain the moment you suspect you have identified a speaker. The organizer behind R08 discloses it to the committee before scoring and abstains on it and on anything from their own employer.",
"arbitration": "Self-set: the anonymizer, who never scores and so holds no stake in any grade, arbitrates when a reviewer declines to recuse from a disclosed conflict.",
"fallback": "Self-set: a proposal that loses too many graders to conflict or abstention to produce a fair letter grade gets reassigned to a fresh grader rather than scored on an incomplete read."
},
"rounds": {
"cut_rule": "Advance every proposal with at least one A to the discussion round: AA generally gets in, AB usually does, and no A means no place.",
"steps": [
"Every proposal gets a full read and written comments from each assigned grader, with abstentions recorded for conflicts and suspected identity.",
"Apply the cut rule, then hold the one synchronous committee discussion round over everything that advanced.",
"Run the program-fit pass on the shortlist only: vendor lesson-transfer, freshness, depth gradient, panel composition, and employer and first-time balance.",
"If both R01 and R09 clear the cut, let their shared speaker choose which one to give."
]
},
"program_fit": [
"Lesson-transfer test on R04 (reads like a product talk, sponsor-affiliated under the one-slot ceiling): if the audience forgot the product name tomorrow, would they keep the lesson? A commercial demo fails outright.",
"Freshness check on R06 (promises the latest or the state of the art): verify its claims still hold against the event date rather than the submission date.",
"Depth-gradient check for the mixed room: every accepted talk should open where nearly everyone follows and close on material most of the room cannot yet follow.",
"Panel-composition gate on R10 (all panelists from City Council): no panel of three or more may come from a single organization, and it needs at least one panelist who does not identify as a man.",
"First-time-speaker balance: first-time speakers are 33.3% of the pasted pool against a published aim of at least 50% new speakers where possible - decide whether that aim is a goal or a gate before review."
],
"waitlist": {
"size": 5,
"order": "Build the ordered waitlist of about five backup talks before any notification goes out, and promise backup talks a date rather than odds of a slot opening."
},
"notifications": [
{
"when": "2027-02-01",
"who": "accepted speakers",
"what": "Notify acceptance with a confirmation deadline of 2027-02-08."
},
{
"when": "2027-02-01",
"who": "waitlisted speakers",
"what": "Send ordered waitlist notices at the same time as acceptances, with a date rather than odds."
},
{
"when": "2027-02-09",
"who": "declined submitters",
"what": "Send declines with a criteria recap and the acceptance ratio once acceptances are confirmed."
},
{
"when": "after declines",
"who": "everyone",
"what": "Announce the full program once declines have gone out."
}
],
"decline": {
"rung": "recap",
"why": "At roughly 5.3:1 most no's are a function of the ratio rather than a judgment on quality, so a warm recap with the published criteria and the ratio does the honest work without promising feedback the committee has no hours to write."
},
"metrics": [
"Actual committee reading hours spent against the 48-hour budget.",
"Final first-time-speaker share against the 50% aim.",
"Employer and local concentration in the accepted set versus the pasted pool.",
"Waitlist offers made, accepted, and whether the promised date held."
],
"caveats": [
"Reading load is about 53 reviewer-hours against 48 available hours for the committee - close this capacity gap before review opens.",
"R10's panel is entirely from City Council, which the composition rule forbids, and needs at least one panelist who does not identify as a man - a gate at selection, not a score."
]
}
The page's check: kept - nothing to flag
A blind single-track community day - anonymize
Committee consensus, anonymization anonymizer, 5 pasted proposals carry identity in their text.
Input:
{
"task": "anonymize",
"event": "Event: Riverside Python Day 2027 - community conference; 1 track; 12 slots\nCall promised: blind review; financial-assistance requests on the form: yes; live fairness complaint from last edition: no\nCommittee: 6 reviewers, 8 hours each; edition compounds year over year: yes; who is in the room: mixed\nDates: today 2027-01-10; promised notification 2027-02-01\nCaps: organizer-given talks not set; sponsor-affiliated slots 1",
"pool": "Received: 64 proposals for 12 slots - 5.3:1, acceptance 18.8%. Calibration (not targets): PyCon US 2025 938 proposals, about 15% selected (about 6.5:1); KubeCon EU 2024 2,541 submissions, 223 accepted, 9%; DjangoCon Europe 2024 174 proposals from 138 submitters, 51 first-time speakers; CHI 2024 (academic) 26.4% - a different selection logic that does not transfer\nPasted rows: 12; first-time speakers 33.3%; local 66.7%; most-represented employers Brightwell 3, Lumen Freight 2, Flumewick 1\nLetter grades: 6 of 12 graded proposals hold at least one A; 1 are AA\nProposals (id | title | flags):\nR01 | Why our build cache kept lying to us | first-time; grades AC\nR02 | Typing a 200k-line Django codebase, one module at a time | identity in text: speaker name/employer; grades AB\nR03 | Async Python without the footguns | identity in text: link; grades BC\nR04 | Introducing Flumewick Cloud: pipelines in five minutes | identity in text: employer; sponsor-affiliated; reads like a product talk; grades CD\nR05 | Teaching Python to nurses | identity in text: gendered pronoun; first-time; grades AB\nR06 | The state of the art in Python packaging | promises the latest; grades BC\nR07 | Profiling NumPy code you did not write | identity in text: handle; grades AC\nR08 | Running the meetup: what five years of PyRiverside taught us | organizer; grades BB\nR09 | Property-based testing for data pipelines | first-time; grades AA\nR10 | Panel: Python in public services | grades BC\nR11 | Writing a tokenizer by hand | first-time; grades AB\nR12 | Debugging memory leaks in long-running workers | grades BD",
"facts": "Committee model: consensus - Small consensus committee; 1 anonymizer(s) who never score, 5 graders; reading load about 53 reviewer-hours against 48 available\nRubric: Letter-grade advocate scale (A advocate, B like, C neutral, D against, X abstain - suspected identity)\nAnonymization depth: anonymizer - Dedicated anonymizer (sees raw submissions, never scores, rewrites identifying details into placeholders, assigns random IDs). Financial-assistance requests ride on the same form: anonymization is what enforces the promise that asking for money does not hurt a proposal.\nCut rule: Advance every proposal with at least one A to the discussion round; AA generally gets in, AB usually does; no A, no place.\nWaitlist: 5 - about five backup talks - the one documented size, for a single-track program\nDecline rung: recap - Decline plus criteria recap (one warm template, the published criteria, the acceptance ratio) (one warm template with the published criteria and the acceptance ratio - the ratio reframes a no as arithmetic)\nNotification dates: acceptances and waitlist notices 2027-02-01 with confirmation by 2027-02-08 (seven days, self-set); declines 2027-02-09 once acceptances are confirmed; program announcement after that\nFindings (15):\n[fix] capacity (committee): Reading the pool takes about 53 reviewer-hours (64 proposals x 5 reads x 10 minutes, a self-set reading rate) and the committee has 48 - a committee that cannot read the pool inside the calendar is a process failure discovered too late.\n[check] leaks (anonymization): 5 proposals of 12 carry identity in their own text (1 speaker name, 2 employers, 1 link, 1 gendered pronoun, 1 handle) - the anonymizer's rewrite list.\n[info] x_grade (anonymization): Reviewers grade X and abstain when they suspect they have identified a speaker - it covers suspected identity only, not a declared employer or friendship.\n[info] blind_limit (anonymization): No evidence exists that anonymized review changes acceptance outcomes by demographic; blind review is a process commitment, not a demonstrated equity outcome. Say so in the plan.\n[check] conflicts (conflicts): 7 reviewer-proposal pairs share an employer - those reviewers skip the question rather than score and disclose: Sam Turner on R08, Hana Ito on R02, Hana Ito on R07, Hana Ito on R12, Oluwaseun Ade on R01, Oluwaseun Ade on R09, ....\n[info] coi_honour (conflicts): Every recusal rule on record is honour-system; any arbitration or audit you add is your own design. Decide now who arbitrates a reviewer who will not recuse, and what happens to a proposal that loses too many reviewers.\n[check] organizer_subs (conflicts): 1 organizer-submitted proposal (R08): label it to the committee before scoring opens; the organizer abstains on their own and their employer's; no cap on organizer talks is set - write one down, or the team quietly becomes the program.\n[info] sponsor_subs (conflicts): 1 sponsor-affiliated proposal goes through the same review - no speaker slot is bought, ever; ceiling 1.\n[info] duplicates (pool): 1 speaker submitted more than once (R01/R09) - if more than one is accepted, let them choose which to give.\n[fix] panel (balance): R10 is a panel whose panelists all come from City Council - the composition rule bans a single-organization panel. A panel of three or more also needs at least one speaker who does not identify as a man - a gate at selection, not a score.\n[check] vendor_talks (fit): 1 proposal reads like a product talk (R04) - apply the lesson-transfer test: if the audience forgot the product name tomorrow, would they keep the lesson? A commercial demo fails outright.\n[info] freshness (fit): 1 proposal promises the latest or the state of the art (R06) - check each against the event date, not the submission date; consider holding a slot or two to fill late (self-set).\n[check] depth_gradient (fit): The room is mixed: brief every accepted talk to open where nearly everyone follows and end on material most of the room cannot - if a talk did not lose most of the room by the end, it did not go deep enough. Score proposals on whether they can carry both ends.\n[info] first_time (balance): First-time speakers are 33.3% of the pasted pool. One published event-level aim is at least 50% new speakers if possible - a goal, not a rubric line; decide before review whether yours is a goal or a gate.\n[info] subset (pool): The pasted rows (12) are a subset of the 64 received; counts below describe the pasted rows only.",
"proposals": "Batch 1 of 1\n\nID: R02\nTitle: Typing a 200k-line Django codebase, one module at a time\nAbstract: How Priya Raman's team at Brightwell moved a legacy Django monolith to strict typing without a freeze: the ratchet file, the CI gate and the modules we gave up on.\nFound: speaker name \"Priya Raman\"; employer \"Brightwell\"\n\nID: R03\nTitle: Async Python without the footguns\nAbstract: Cancellation, timeouts and task groups explained with failures from production. Code on github.com/tomaszw/async-footguns, slides after the talk.\nFound: link \"github.com/tomaszw/async-footguns\"\n\nID: R04\nTitle: Introducing Flumewick Cloud: pipelines in five minutes\nAbstract: A live demo of our platform: connect a source, transform with our SDK and ship to the warehouse. Free trial for every attendee.\nFound: employer \"Flumewick\"\n\nID: R05\nTitle: Teaching Python to nurses\nAbstract: She started a Python club on a hospital night shift. What worked, what did not, and the three scripts that saved her ward an hour a day.\nFound: gendered pronoun \"She, her\"\n\nID: R07\nTitle: Profiling NumPy code you did not write\nAbstract: A method for finding the slow line in someone else's numerical code: sampling first, then line profiling, then a memory view. Follow me @lena_profiles for the notebook.\nFound: handle \"@lena_profiles\""
}
Reply:
{
"task": "anonymize",
"items": [
{
"id": "R02",
"title": "Typing a 200k-line Django codebase, one module at a time",
"abstract": "How [Speaker]'s team at [Large Tech Firm] moved a legacy Django monolith to strict typing without a freeze: the ratchet file, the CI gate and the modules we gave up on.",
"placeholders": [
"[Speaker]",
"[Large Tech Firm]"
],
"kept": ""
},
{
"id": "R03",
"title": "Async Python without the footguns",
"abstract": "Cancellation, timeouts and task groups explained with failures from production. Code on [link removed], slides after the talk.",
"placeholders": [
"[link removed]"
],
"kept": ""
},
{
"id": "R04",
"title": "Introducing [Large Tech Firm] Cloud: pipelines in five minutes",
"abstract": "A live demo of our platform: connect a source, transform with our SDK and ship to the warehouse. Free trial for every attendee.",
"placeholders": [
"[Large Tech Firm]"
],
"kept": ""
},
{
"id": "R05",
"title": "Teaching Python to nurses",
"abstract": "[They] started a Python club on a hospital night shift. What worked, what did not, and the three scripts that saved [their] ward an hour a day.",
"placeholders": [
"[They]",
"[their]"
],
"kept": ""
},
{
"id": "R07",
"title": "Profiling NumPy code you did not write",
"abstract": "A method for finding the slow line in someone else's numerical code: sampling first, then line profiling, then a memory view. Follow me [handle removed] for the notebook.",
"placeholders": [
"[handle removed]"
],
"kept": ""
}
],
"note": "Hand-check R04's abstract for residual self-referencing language like our platform or our SDK that may still read as a vendor pitch despite the employer redaction."
}
The page's check: kept - nothing to flag
A three-track summit with star grades - plan
Committee cascade, anonymization platform, 4 pasted proposals carry identity in their text.
Input:
{
"task": "plan",
"event": "Event: Northwind Cloud Summit - community conference; 3 tracks; 60 slots\nCall promised: blind review; financial-assistance requests on the form: no; live fairness complaint from last edition: no\nCommittee: 24 reviewers, 10 hours each; edition compounds year over year: yes; who is in the room: experts\nDates: today 2027-02-01; promised notification 2027-03-15\nCaps: organizer-given talks 2; sponsor-affiliated slots 4",
"pool": "Received: 640 proposals for 60 slots - 10.7:1, acceptance 9.4%. Calibration (not targets): PyCon US 2025 938 proposals, about 15% selected (about 6.5:1); KubeCon EU 2024 2,541 submissions, 223 accepted, 9%; DjangoCon Europe 2024 174 proposals from 138 submitters, 51 first-time speakers; CHI 2024 (academic) 26.4% - a different selection logic that does not transfer\nPasted rows: 10; first-time speakers 40%; local 30%; most-represented employers Ferrovia 3, Tallorix 3, Northbank 2; by track Data 1, Platform 5, Security 2, SRE 2\nStar grades: top 30% of the graded proposals = 3 (S06, S01, S05)\nProposals (id | title | flags):\nS01 | Zero-downtime Postgres major upgrades | stars 4.3\nS02 | Platform teams that do not become ticket queues | stars 3.8\nS03 | Kubernetes cost allocation that finance believes | first-time; stars 3.5\nS04 | Tallorix Observability: see everything in one pane | identity in text: employer; stars 2\nS05 | The latest in eBPF for security teams | identity in text: employer; promises the latest; stars 4\nS06 | On-call that people volunteer for | first-time; stars 4.7\nS07 | Supply-chain attestations in practice | identity in text: link; stars 3.8\nS08 | GPU scheduling for mixed training and inference | first-time; stars 3.5\nS09 | Migrating 2,000 Terraform modules | stars 3.2\nS10 | SRE for teams of three | identity in text: gendered pronoun; first-time; stars 4",
"facts": "Committee model: cascade - Multi-tier cascade; reading load about 320 reviewer-hours against 240 available\nRubric: Multi-criteria star scale (content, originality, relevance, speaker; five stars in half steps, written feedback on every rating)\nAnonymization depth: platform - Platform-level hiding (the tool withholds the author field; text the author wrote is untouched). The default where the call promised blind review: the tool hides the author field. State its limit to submitters before the call closes - it does not hide what the author wrote.\nCut rule: Top 30% of each track to the track chairs, who pass roughly 30% of those (about 9% of the pool) with mandatory comments to the co-chairs.\nWaitlist: 15 - about five per track (15) - extrapolated from the single-track figure\nDecline rung: feedback - Per-proposal written feedback (reviewer comments, edited for tone) (a first-time-heavy pool, where growing the speaker base is the point)\nNotification dates: acceptances and waitlist notices 2027-03-15 with confirmation by 2027-03-22 (seven days, self-set); declines 2027-03-23 once acceptances are confirmed; program announcement after that\nFindings (9):\n[fix] capacity (committee): Reading the pool takes about 320 reviewer-hours (640 proposals x 3 reads x 10 minutes, a self-set reading rate) and the committee has 240 - a committee that cannot read the pool inside the calendar is a process failure discovered too late.\n[fix] leaks (anonymization): 4 proposals of 10 carry identity in their own text (2 employers, 1 link, 1 gendered pronoun) - platform hiding will not remove any of it. Tell submitters before the call closes, or move to a dedicated anonymizer.\n[info] blind_limit (anonymization): No evidence exists that anonymized review changes acceptance outcomes by demographic; blind review is a process commitment, not a demonstrated equity outcome. Say so in the plan.\n[check] conflicts (conflicts): 5 reviewer-proposal pairs share an employer - those reviewers skip the question rather than score and disclose: Mira Holt on S01, Mira Holt on S03, Mira Holt on S05, Leo Grant on S06, Leo Grant on S08.\n[info] coi_honour (conflicts): Every recusal rule on record is honour-system; any arbitration or audit you add is your own design. Decide now who arbitrates a reviewer who will not recuse, and what happens to a proposal that loses too many reviewers.\n[info] duplicates (pool): 1 speaker submitted more than once (S01/S05) - if more than one is accepted, let them choose which to give.\n[info] freshness (fit): 1 proposal promises the latest or the state of the art (S05) - check each against the event date, not the submission date; consider holding a slot or two to fill late (self-set).\n[info] first_time (balance): First-time speakers are 40% of the pasted pool. One published event-level aim is at least 50% new speakers if possible - a goal, not a rubric line; decide before review whether yours is a goal or a gate.\n[info] subset (pool): The pasted rows (10) are a subset of the 640 received; counts below describe the pasted rows only.",
"proposals": "none"
}
Reply:
{
"task": "plan",
"summary": "At 10.7:1 (640 proposals, 60 slots), a 24-reviewer cascade committee across three tracks must first close a reading-capacity gap of 320 reviewer-hours needed against 240 available.",
"committee": {
"model": "cascade",
"roles": [
"Track committees - the 24 reviewers split across 3 tracks - score every proposal in their own track with written comments, don't see other tracks' pools",
"Track chairs - one per track - advance the top 30% of their track's scores with mandatory comments, don't start scoring from zero",
"Co-chairs - final decision body - decide on the roughly 9% of the pool that reaches them, don't re-score the full pool"
],
"why": "Required for a 3-track, 640-proposal pool too large for any single room to read together."
},
"rubric": {
"name": "Multi-criteria star scale",
"scale": [
"Content - depth and rigor, scored in half-star steps",
"Originality - novelty of the angle, scored in half-star steps",
"Relevance - fit for the track and audience, scored in half-star steps",
"Speaker - preparedness and delivery, scored in half-star steps"
],
"aggregation": "The four criteria combine into one overall star rating per proposal, with written feedback required on every rating, and proposals are ranked by that rating within their track (for example S06 4.7, S01 4.3, S05 4.0)."
},
"anonymization": {
"depth": "platform",
"owner_role": "the program chair administering the platform's author-hiding, since no dedicated anonymizer is appointed",
"identity_enters": "Accepted speakers have their identity restored once confirmed, to handle logistics; declines go out without ever telling reviewers who the submitter was.",
"limit": "Platform hiding withholds only the author field, so it won't touch the 4 of 10 pasted proposals that leak identity in their own wording (2 employers, 1 link, 1 gendered pronoun); outcome data tying blind review to different acceptance rates by demographic doesn't exist, so call it a process commitment rather than a proven result."
},
"conflicts": {
"brief": "Skip scoring instead of disclosing for a close colleague or an organization you belong to or compete with, and do the same the moment you think you've worked out who the speaker is; a lesser conflict you choose to keep scoring through still needs a note in the review text. Five reviewer-proposal pairs share an employer this round.",
"arbitration": "The co-chairs decide when a reviewer declines to recuse despite a disclosed conflict (self-set).",
"fallback": "A proposal that loses too many reviewers to abstentions gets a substitute reader assigned from outside the conflicted track before the chairs see it (self-set)."
},
"rounds": {
"cut_rule": "Top 30% of each track advance to the track chairs, who pass roughly 30% of those - about 9% of the pool - to the co-chairs with mandatory comments.",
"steps": [
"Track committees score every proposal in their track with written comments.",
"Top 30% per track advance to the track chairs.",
"Track chairs pass roughly 30% of those (about 9% of the pool) to the co-chairs with mandatory comments.",
"Co-chairs run the program-fit pass on the shortlist and decide."
]
},
"program_fit": [
"S05 - The latest in eBPF for security teams: promises the latest - check it against the event date, not the submission date, and consider holding a slot or two to fill late.",
"S01/S05 - same speaker submitted both; if both are accepted, let the speaker choose which to give.",
"First-time speakers are 40% of the pasted pool against the published aim of at least 50% - decide before review whether that's a goal or a gate.",
"Employer and local spread: Ferrovia (3) and Tallorix (3) lead the pasted pool, which is only 30% local - watch employer and geographic balance across tracks."
],
"waitlist": {
"size": 15,
"order": "Built before any notification goes out, about five backups per track across the 3 tracks, extrapolated from the single-track figure, with backup speakers promised a date, never odds."
},
"notifications": [
{
"when": "2027-03-15",
"who": "accepted speakers",
"what": "Acceptance sent with a confirmation deadline of 2027-03-22."
},
{
"when": "2027-03-15",
"who": "waitlisted speakers",
"what": "Waitlist notice sent at the same time as acceptances, ordered within track, with a date to hear back."
},
{
"when": "2027-03-23",
"who": "declined submitters",
"what": "Decline sent with per-proposal written feedback, edited reviewer comments, once acceptances are confirmed."
},
{
"when": "after declines are sent",
"who": "all submitters and the public",
"what": "Program announcement goes out once the decline round is complete."
}
],
"decline": {
"rung": "feedback",
"why": "The pasted pool is 40% first-time speakers, and growing the speaker base is the point; the star rubric already requires written comments on every rating, so turning those into per-proposal decline feedback costs little extra."
},
"metrics": [
"Submission-to-slot ratio and acceptance rate against this edition's 10.7:1 and 9.4%.",
"First-time speaker share against the 50% aim (40% this round).",
"Reviewer-hours actually spent against the 320-hour estimate.",
"Share of accepted proposals that still carried an unresolved identity leak at submission."
],
"caveats": [
"Reading the pool takes about 320 reviewer-hours (640 proposals x 3 reads x 10 minutes) against 240 available - close this capacity gap before review opens, not mid-round.",
"4 of the 10 pasted proposals leak identity in their own text (2 employers, 1 link, 1 gendered pronoun) that platform-level hiding will not remove - tell submitters before the call closes, or move to a dedicated anonymizer."
]
}
The page's check: kept - nothing to flag
A three-track summit with star grades - anonymize
Committee cascade, anonymization platform, 4 pasted proposals carry identity in their text.
Input:
{
"task": "anonymize",
"event": "Event: Northwind Cloud Summit - community conference; 3 tracks; 60 slots\nCall promised: blind review; financial-assistance requests on the form: no; live fairness complaint from last edition: no\nCommittee: 24 reviewers, 10 hours each; edition compounds year over year: yes; who is in the room: experts\nDates: today 2027-02-01; promised notification 2027-03-15\nCaps: organizer-given talks 2; sponsor-affiliated slots 4",
"pool": "Received: 640 proposals for 60 slots - 10.7:1, acceptance 9.4%. Calibration (not targets): PyCon US 2025 938 proposals, about 15% selected (about 6.5:1); KubeCon EU 2024 2,541 submissions, 223 accepted, 9%; DjangoCon Europe 2024 174 proposals from 138 submitters, 51 first-time speakers; CHI 2024 (academic) 26.4% - a different selection logic that does not transfer\nPasted rows: 10; first-time speakers 40%; local 30%; most-represented employers Ferrovia 3, Tallorix 3, Northbank 2; by track Data 1, Platform 5, Security 2, SRE 2\nStar grades: top 30% of the graded proposals = 3 (S06, S01, S05)\nProposals (id | title | flags):\nS01 | Zero-downtime Postgres major upgrades | stars 4.3\nS02 | Platform teams that do not become ticket queues | stars 3.8\nS03 | Kubernetes cost allocation that finance believes | first-time; stars 3.5\nS04 | Tallorix Observability: see everything in one pane | identity in text: employer; stars 2\nS05 | The latest in eBPF for security teams | identity in text: employer; promises the latest; stars 4\nS06 | On-call that people volunteer for | first-time; stars 4.7\nS07 | Supply-chain attestations in practice | identity in text: link; stars 3.8\nS08 | GPU scheduling for mixed training and inference | first-time; stars 3.5\nS09 | Migrating 2,000 Terraform modules | stars 3.2\nS10 | SRE for teams of three | identity in text: gendered pronoun; first-time; stars 4",
"facts": "Committee model: cascade - Multi-tier cascade; reading load about 320 reviewer-hours against 240 available\nRubric: Multi-criteria star scale (content, originality, relevance, speaker; five stars in half steps, written feedback on every rating)\nAnonymization depth: platform - Platform-level hiding (the tool withholds the author field; text the author wrote is untouched). The default where the call promised blind review: the tool hides the author field. State its limit to submitters before the call closes - it does not hide what the author wrote.\nCut rule: Top 30% of each track to the track chairs, who pass roughly 30% of those (about 9% of the pool) with mandatory comments to the co-chairs.\nWaitlist: 15 - about five per track (15) - extrapolated from the single-track figure\nDecline rung: feedback - Per-proposal written feedback (reviewer comments, edited for tone) (a first-time-heavy pool, where growing the speaker base is the point)\nNotification dates: acceptances and waitlist notices 2027-03-15 with confirmation by 2027-03-22 (seven days, self-set); declines 2027-03-23 once acceptances are confirmed; program announcement after that\nFindings (9):\n[fix] capacity (committee): Reading the pool takes about 320 reviewer-hours (640 proposals x 3 reads x 10 minutes, a self-set reading rate) and the committee has 240 - a committee that cannot read the pool inside the calendar is a process failure discovered too late.\n[fix] leaks (anonymization): 4 proposals of 10 carry identity in their own text (2 employers, 1 link, 1 gendered pronoun) - platform hiding will not remove any of it. Tell submitters before the call closes, or move to a dedicated anonymizer.\n[info] blind_limit (anonymization): No evidence exists that anonymized review changes acceptance outcomes by demographic; blind review is a process commitment, not a demonstrated equity outcome. Say so in the plan.\n[check] conflicts (conflicts): 5 reviewer-proposal pairs share an employer - those reviewers skip the question rather than score and disclose: Mira Holt on S01, Mira Holt on S03, Mira Holt on S05, Leo Grant on S06, Leo Grant on S08.\n[info] coi_honour (conflicts): Every recusal rule on record is honour-system; any arbitration or audit you add is your own design. Decide now who arbitrates a reviewer who will not recuse, and what happens to a proposal that loses too many reviewers.\n[info] duplicates (pool): 1 speaker submitted more than once (S01/S05) - if more than one is accepted, let them choose which to give.\n[info] freshness (fit): 1 proposal promises the latest or the state of the art (S05) - check each against the event date, not the submission date; consider holding a slot or two to fill late (self-set).\n[info] first_time (balance): First-time speakers are 40% of the pasted pool. One published event-level aim is at least 50% new speakers if possible - a goal, not a rubric line; decide before review whether yours is a goal or a gate.\n[info] subset (pool): The pasted rows (10) are a subset of the 640 received; counts below describe the pasted rows only.",
"proposals": "Batch 1 of 1\n\nID: S04\nTitle: Tallorix Observability: see everything in one pane\nAbstract: A tour of Tallorix Observability's new features with a live demo and pricing for teams.\nFound: employer \"Tallorix\"\n\nID: S05\nTitle: The latest in eBPF for security teams\nAbstract: What eBPF can see that your agents cannot, with three detections we wrote at Ferrovia.\nFound: employer \"Ferrovia\"\n\nID: S07\nTitle: Supply-chain attestations in practice\nAbstract: SLSA levels, Sigstore and the build changes that made attestations cheap; repo at github.com/rkoh/attest-demo.\nFound: link \"github.com/rkoh/attest-demo\"\n\nID: S10\nTitle: SRE for teams of three\nAbstract: What a startup SRE function looks like before it can afford one: budgets, alerts and the runbook he wishes he had written first.\nFound: gendered pronoun \"he, he\""
}
Reply:
{
"task": "anonymize",
"items": [
{
"id": "S04",
"title": "[Employer] Observability: see everything in one pane",
"abstract": "A tour of [Employer] Observability's new features with a live demo and pricing for teams.",
"placeholders": [
"[Employer]"
],
"kept": ""
},
{
"id": "S05",
"title": "The latest in eBPF for security teams",
"abstract": "What eBPF can see that your agents cannot, with three detections we wrote at [Employer].",
"placeholders": [
"[Employer]"
],
"kept": ""
},
{
"id": "S07",
"title": "Supply-chain attestations in practice",
"abstract": "SLSA levels, Sigstore and the build changes that made attestations cheap; repo at [link removed].",
"placeholders": [
"[link removed]"
],
"kept": ""
},
{
"id": "S10",
"title": "SRE for teams of three",
"abstract": "What a startup SRE function looks like before it can afford one: budgets, alerts and the runbook [they] wishes [they] had written first.",
"placeholders": [
"[they]"
],
"kept": ""
}
],
"note": "Spot check S04 and S05 employer placeholders in both title and abstract, and confirm S10's pronoun swap reads sensibly if you tidy verb agreement by hand."
}
The page's check: kept - nothing to flag
A small meetup, named review - plan
Committee solo, anonymization named, 0 pasted proposals carry identity in their text.
Input:
{
"task": "plan",
"event": "Event: Harbour Python monthly - spring season - community conference; 1 track; 6 slots\nCall promised: named review; financial-assistance requests on the form: no; live fairness complaint from last edition: no\nCommittee: 2 reviewers, 3 hours each; edition compounds year over year: no; who is in the room: newcomers\nDates: today 2027-02-20; promised notification 2027-03-01\nCaps: organizer-given talks not set; sponsor-affiliated slots not set",
"pool": "Received: 8 proposals for 6 slots - 1.3:1, acceptance 75%. Calibration (not targets): PyCon US 2025 938 proposals, about 15% selected (about 6.5:1); KubeCon EU 2024 2,541 submissions, 223 accepted, 9%; DjangoCon Europe 2024 174 proposals from 138 submitters, 51 first-time speakers; CHI 2024 (academic) 26.4% - a different selection logic that does not transfer\nPasted rows: 8; most-represented employers Kestrova Data 2, Harbourline 2\nProposals (id | title | flags):\nP001 | Notebooks to packages | -\nP002 | Pandas 3 changes you will hit | -\nP003 | A tiny web app with htmx | -\nP004 | Python on a Raspberry Pi greenhouse | -\nP005 | Pydantic v2 in a legacy codebase | -\nP006 | Speeding up tests with pytest-xdist | -\nP007 | Type hints for scientists | -\nP008 | Our product: Kestrova Insights | reads like a product talk",
"facts": "Committee model: solo - Solo curation; reading load about 1 reviewer-hours against 6 available\nRubric: No shared rubric - one curator's editorial judgment, with the published criteria as the record\nAnonymization depth: named - Named review (the call did not promise anonymization). The call did not promise anonymization, so it is off the menu for this edition - retrofitting it mid-review anonymizes nothing. Named review is only honest when the published criteria carry the fairness load.\nCut rule: The curator's picks against the published criteria, with each pick's reason recorded.\nWaitlist: 2 - every proposal not accepted (2) - fewer than the documented five\nDecline rung: feedback - Per-proposal written feedback (reviewer comments, edited for tone) (the pool is small enough to write them all (60 or fewer, self-set))\nNotification dates: acceptances and waitlist notices 2027-03-01 with confirmation by 2027-03-08 (seven days, self-set); declines 2027-03-09 once acceptances are confirmed; program announcement after that\nFindings (4):\n[info] coi_honour (conflicts): Every recusal rule on record is honour-system; any arbitration or audit you add is your own design. Decide now who arbitrates a reviewer who will not recuse, and what happens to a proposal that loses too many reviewers.\n[info] duplicates (pool): 2 speakers submitted more than once (P001/P003, P002/P008) - if more than one is accepted, let them choose which to give.\n[check] vendor_talks (fit): 1 proposal reads like a product talk (P008) - apply the lesson-transfer test: if the audience forgot the product name tomorrow, would they keep the lesson? A commercial demo fails outright.\n[info] room_level (fit): The room is mostly people moving into the field: pitch each talk's deep end at that room - a program built for experts who do not come loses the people who do.",
"proposals": "none"
}
Reply:
{
"task": "plan",
"summary": "At 1.3:1 (8 proposals for 6 slots), one curator decides alone under named review; settle first whether P008's product demo passes the lesson-transfer test.",
"committee": {
"model": "solo",
"roles": [
"Curator - 1 - reads all 8 proposals alone and decides accept, waitlist or decline against the published criteria",
"Curator - 1 - records the reason for every pick and writes the per-proposal decline feedback"
],
"why": "A pool of 8 proposals for 6 slots is small enough for one curator to read and decide alone, with the published criteria - not a hidden rubric - doing the fairness work since the call did not promise anonymity."
},
"rubric": {
"name": "No shared rubric: curator's editorial judgment",
"scale": [
"No shared scale - every pick is checked against the published criteria instead"
],
"aggregation": "There is nothing to aggregate: a single curator decides each pick alone, with the reason recorded individually rather than combined across reviewers."
},
"anonymization": {
"depth": "named",
"owner_role": "Curator",
"identity_enters": "Identity is visible throughout, since the call never promised anonymity, so there is no re-entry step - the curator reads every submission named from the start.",
"limit": "Without anonymity, any curator leaning toward or against a submitter they recognize is not screened for, retrofitting anonymization mid-review would anonymize nothing at this point, and the published criteria are the only check left on fairness."
},
"conflicts": {
"brief": "The curator recuses rather than scores and discloses on any proposal tied to a close colleague or an organization they belong to or compete with, since no second reader is in the loop under solo curation to catch it.",
"arbitration": "Self-set: with no second reviewer in the loop, a designated backup outside the curator role decides if the curator will not recuse from a disclosed conflict.",
"fallback": "Self-set: if the curator recuses from a proposal, it is held aside for the backup decision-maker to judge alone rather than scored by the recused curator."
},
"rounds": {
"cut_rule": "The curator's picks against the published criteria, with each pick's reason recorded.",
"steps": [
"The curator reads every one of the 8 proposals and weighs each one against the published criteria.",
"Apply the cut rule to settle accepts, waitlist and declines, recording the reason for every pick.",
"Run the program-fit pass on the shortlist: the vendor lesson-transfer test, and pitching every accepted talk's deep end at a newcomer room.",
"If both duplicate-submitter pairs (P001/P003 and P002/P008) clear the cut, let each speaker choose which proposal to give."
]
},
"program_fit": [
"Lesson-transfer test on P008 (Our product: Kestrova Insights, reads like a product talk): if the audience forgot the product name tomorrow, would they keep the lesson? A commercial demo fails outright.",
"Room-level pitch check: the room is mostly newcomers moving into the field, so pitch every accepted talk's deep end there rather than at a room of experts who did not come.",
"Employer spread: Kestrova Data and Harbourline each account for 2 of the 8 pasted proposals - weigh that concentration against the 6 slots alongside the room-level and lesson-transfer checks."
],
"waitlist": {
"size": 2,
"order": "Order the 2 not-accepted proposals as the waitlist before telling anyone, note that this is fewer than the usual five-backup benchmark, and promise a date rather than odds."
},
"notifications": [
{
"when": "2027-03-01",
"who": "accepted speakers",
"what": "Notify acceptance with a confirmation deadline of 2027-03-08."
},
{
"when": "2027-03-01",
"who": "waitlisted speakers",
"what": "Send the ordered waitlist notices at the same time as acceptances, with a date rather than odds."
},
{
"when": "2027-03-09",
"who": "declined submitters",
"what": "Send per-proposal written decline feedback, edited for tone, once acceptances are confirmed."
},
{
"when": "after declines",
"who": "everyone",
"what": "Announce the full program once declines have gone out."
}
],
"decline": {
"rung": "feedback",
"why": "At 8 proposals, the pool is well inside the committee's own threshold for writing every decline personal feedback, so edited reviewer comments go out rather than a form note."
},
"metrics": [
"Accept, waitlist and decline reasons recorded per pick, to check consistency against the published criteria next time.",
"Employer concentration (Kestrova Data, Harbourline) in the accepted set versus the pasted pool.",
"Whether the per-proposal decline feedback stays sustainable if the pool grows past this edition's size."
],
"caveats": []
}
The page's check: kept - nothing to flag
The identity-leak CSV for the first sample
id,title,leak,value
R02,"Typing a 200k-line Django codebase, one module at a time",speaker name,Priya Raman
R02,"Typing a 200k-line Django codebase, one module at a time",employer,Brightwell
R03,Async Python without the footguns,link,github.com/tomaszw/async-footguns
R04,Introducing Flumewick Cloud: pipelines in five minutes,employer,Flumewick
R05,Teaching Python to nurses,gendered pronoun,"She, her"
R07,Profiling NumPy code you did not write,handle,@lena_profiles
Build the input yourself
Load the engine in node (global.window = global; require("./shortkit.js"); require("./shortreply.js")), then:
const K = ShortKit, R = ShortReply;
const f = K.form({ name: "My Conf", pole: "community", tracks: "1", slots: "12", reviewers: "6", hours: "8", blind: "yes",
today: "2027-01-10", notify: "2027-02-01", pool: fs.readFileSync("cfp-export.csv", "utf8"), reviewerList: "Hana Ito, Brightwell" });
// f.model: consensus | cascade | pool | solo f.depth: anonymizer | platform | named f.rubric, f.cutRule, f.waitlist, f.decline, f.dates
// f.props: [{id, title, abstract, speaker, employer, leaks: [{kind, value}], g: grades}] f.conflicts f.findings
K.leakCSV(f) // id, title, leak, value
const b = K.buildInput(f, "plan"); // or "anonymize" (raw.batch picks the batch of twelve)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "plan");
// R.check(reply, b) // {status: kept or flagged, notes}
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://shortlist-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 = "paste-your-token-here" # from https://shortlist-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"]
// shortlist-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://shortlist-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 = "paste-your-token-here" // from https://shortlist-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
}
// ShortlistDesk.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 ShortlistDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://shortlist-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 = "paste-your-token-here" # from https://shortlist-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, 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://shortlist-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 = "paste-your-token-here"; // from https://shortlist-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":"shortlist-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": "shortlist-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: "shortlist-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": "shortlist-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", "shortlist-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: "shortlist-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" => "shortlist-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 = "shortlist-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.
# input.json: the run input object - {"task": "plan", "event": "Event: ... - community conference; 1 track; 12 slots ...", "pool": "Received: 64 proposals for 12 slots ...", "facts": "Committee model: consensus ...", "proposals": "none"}
# - 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": "plan", "event": "Event: ... - community conference; 1 track; 12 slots ...", "pool": "Received: 64 proposals for 12 slots ...", "facts": "Committee model: consensus ...", "proposals": "none"}
# - 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: "plan" or "anonymize", event, pool, facts, proposals} (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 shortlist-desk:<task>:<inputHash>:a<attempt> - the lane is part of the key - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A plan carries committee, rubric, anonymization, rounds and notifications; an anonymized batch carries items. The code below prints the headline parts (the field names in the generic loops come from the plan).
KEY="shortlist-desk:plan:$(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 '.summary // .format, .gate // .deposit, ((.handoffs // .handoffs)[] | "\(.to // .to): \(.status // .what)")' reply.json
jq -r '.committee.model // empty, .anonymization.depth // empty' reply.json # anything to know before trusting the plan
import hashlib, time
key = "shortlist-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["what"] 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("summary"))
print(reply.get("gate") or reply.get("deposit", ""))
for s in reply.get("handoffs") or reply.get("handoffs") or []:
print(s.get("to", s.get("to")), "-", s.get("status", s.get("what")))
if reply.get("caveats"):
print("caveats:", reply["caveats"])
import { createHash } from "node:crypto";
const key = `shortlist-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 reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.summary);
console.log(reply.gate ?? reply.deposit);
for (const s of reply.handoffs || reply.handoffs || []) console.log(s.to ?? s.to, "-", s.what ?? s.what);
if (reply.caveats) console.log("caveats:", reply.caveats);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "shortlist-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["what"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
items, _ := reply["handoffs"].([]any) // each hand-off is {to, what}
fmt.Println(j["charged_credits"], j["truncated"], reply["summary"])
fmt.Println(reply["gate"], reply["deposit"])
for _, x := range items {
c := x.(map[string]any)
fmt.Println(c["to"], "-", c["what"])
}
if fix, ok := reply["caveats"].(string); ok && fix != "" {
fmt.Println("caveats:", fix)
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "shortlist-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("what").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.get("summary"));
System.out.println(reply.path("gate").asText(reply.path("deposit").asText()));
for (JsonNode s : reply.has("handoffs") ? reply.path("handoffs") : reply.path("handoffs")) System.out.println(s.path("to").asText(s.path("to").asText()) + " - " + s.path("what").asText(s.path("what").asText()));
if (!reply.path("caveats").asText().isEmpty()) System.out.println("caveats: " + reply.get("caveats").asText());
require "digest"
key = "shortlist-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["what"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["summary"]].inspect
puts reply["gate"] || reply["deposit"]
(reply["handoffs"] || reply["handoffs"] || []).each { |s| puts "#{s["to"] || s["to"]} - #{s["what"] || s["what"]}" }
puts "caveats: #{reply["caveats"]}" unless reply["caveats"].to_s.empty?
$key = "shortlist-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["what"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["summary"] ?? ""), "\n";
echo $reply["gate"] ?? $reply["deposit"] ?? "", "\n";
foreach ($reply["handoffs"] ?? $reply["handoffs"] ?? [] as $s) echo $s["to"] ?? $s["to"], " - ", $s["what"] ?? $s["what"], "\n";
if (!empty($reply["caveats"])) echo "caveats: ", $reply["caveats"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"shortlist-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["what"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply!["summary"]}");
Console.WriteLine($"{reply["gate"] ?? reply["deposit"]}");
foreach (var s in (reply["handoffs"] ?? reply["handoffs"]) as JsonArray ?? new JsonArray()) Console.WriteLine($"{s!["to"] ?? s["to"]} - {s["what"] ?? s["what"]}");
if (reply["caveats"] is JsonNode fix && fix.GetValue<string>() != "") Console.WriteLine("caveats: " + fix);
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: shortlist-desk:plan:$(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": "plan", "event": "Event: ... - community conference; 1 track; 12 slots ...", "pool": "Received: 64 proposals for 12 slots ...", "facts": "Committee model: consensus ...", "proposals": "none"}
# - 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": "shortlist-desk:plan:" + 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": `shortlist-desk:plan:${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", "shortlist-desk:plan:"+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", "shortlist-desk:plan:" + 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" => "shortlist-desk:plan:#{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: shortlist-desk:plan:" . 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", $"shortlist-desk:plan:{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);