Run Survey After from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page ties every question to a decision and owner, flags hypothetical and leading wording, picks the depth, the channel and what goes back, checks the anonymity, the 48-hour send and the response rate per role - all free, in surveykit.js. The run writes the memo or the survey; the page then holds the reply to its own design (surveyreply.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":"survey-after"}), 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, instrument, 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: 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 - design (the feedback design memo for the organizers) and survey (the survey participants answer, built from the questions the page keeps). A missing or unknown task is answered as design. Both lanes send the same four fields.
The input
| Field | Required | What |
|---|---|---|
task | yes | design or survey. If it is missing or neither, do design and set "task": "design". |
event | yes | the event, its format and size, whether it already happened, the day's captive moment, the roles and how many were reached or answered, and the promises made. |
instrument | yes | the decisions with owners and due dates, every draft question with its type, role and the decision it serves, the send plan and the close-the-loop plan. |
facts | yes | the page's depth, channel, anonymity, response-rate and close-the-loop picks, the measured bands, the gates, the signals to record and every finding tagged [fix], [check] or [info]. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
- Start from the decision, not a question bank: every question maps to a named decision and the person who makes it, or it is cut. Ask what people did, not what they would hypothetically do; never phrase a question that signals the answer wanted; ask for the one thing to change rather than "any other comments". Keep roughly 70-80% of questions closed and no more than three or four open.
- Depth: a short core of 3-5 questions is the default; a single-question pulse fits a meetup of a few dozen; one role module when a decision is owned by one role; the full role-segmented instrument for a hackathon (team formation, mentors, judging fairness, tooling, continuation) or where sponsors fund most of the budget. Modules for roles that do not exist are deleted. A two- or three-question survey more than doubles the median response of a seven-plus one.
- Anonymity is set before the channel: on whenever questions touch judging fairness, mentors, inclusion or a named person's performance. Anonymous and confidential are different promises - say which. Free text names people: decide who reads it; code-of-conduct allegations leave for that pipeline at once. Publication consent is asked at collection time or not at all.
- Channel: email plus reminders by default; in-room capture (40-60% response against 10-30% emailed) when the day is still being planned, a captive moment exists and the run of show can take one more line - deleted once the event has happened; an incentive (about half again on emailed response) only when a prior rate sat at the floor - deleted under anonymity, because a draw needs an identity. Send within 24-48 hours; at most two reminders. A hackathon gets a second short survey one to two weeks later on whether teams kept working.
- State the response rate beside every finding, per role; below roughly one in ten emailed or four in ten in-room the collection failed - not the audience. Small roles (a handful of sponsors) are read by name, not by percentage. Never average satisfaction with return intent.
- Close the loop: a you-said-we-changed note by default; a public findings post only for a compounding event with a recap planned, and never without publication consent or for a private event. Every threshold here is self-set unless the page names its source.
House rules
- Use only the input and the rules above. The page's names, dates, picks, counts and rates are exact - copy them. Every number in a sentence must appear in the input.
- Label anything that is not one of the rules above, and is not in the input, as your own design.
- Never invent a decision, an owner, a date, a figure, a finding or a person; where someone must be named and is not, write "to be named". Refer to people by name or as they - never infer anyone's gender from a name.
- 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.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
design
summary- one sentence, at most 35 words: the depth, the channel, and the first thing to fix.depth-pickanddraft(pulse,short,moduleorfull: the page's pick and what the draft makes),note(one sentence).channel-pickandplanned(single,reminders,incentiveorinroom: the page's pick and the plan),note(one sentence; name any deleted rung and why).questions- one object per draft question in order:n(its number),keep(true only for a question the page ties to a decision with an owner),decision(the decision it serves exactly, or ""),note(one short sentence: keep as is, rephrase - say how - or cut and why).anonymity-promise(anonymous,confidentialornamed, as planned),note(one sentence: what the questions need and what the instrument must say).timing-send(the planned first send date exactly, or "" for in-room or unset),reminders(the planned reminder dates exactly),followup(the follow-up date, or ""),note(one sentence).loop-pickandplanned(nothing,ack,noteorpublic),note(one sentence).rates- one object per role the page reports a response rate for:role(exactly),rate(the page's percentage as a number, or null for a small role read by name),note(one short sentence).[]before fielding.signals- 2 or 3 short strings, the page's signals, each with what it would change next edition (your design).caveats- at most 4 short strings:[fix]findings first (the most urgent four when there are more), then the most important[check]findings.[]when there are none of either.
{
"task": "design",
"summary": "one sentence",
"depth": { "pick": "short", "draft": "short", "note": "one sentence" },
"channel": { "pick": "reminders", "planned": "reminders", "note": "one sentence" },
"questions": [ { "n": 1, "keep": true, "decision": "a decision", "note": "one short sentence" } ],
"anonymity": { "promise": "anonymous", "note": "one sentence" },
"timing": { "send": "YYYY-MM-DD", "reminders": [ "YYYY-MM-DD" ], "followup": "", "note": "one sentence" },
"loop": { "pick": "note", "planned": "note", "note": "one sentence" },
"rates": [],
"signals": [ "what to record and what it changes" ],
"caveats": [ "a caveat" ]
}
The page holds it to its own design: the depth, channel and close-the-loop picks beside the planned ones, every question kept or cut exactly as the page ties it to an owned decision, the anonymity promise, the planned send and reminder dates and every per-role response rate; no figure may appear that the input does not hold.
survey
title- a short title naming the event.intro- two or three sentences: why the survey exists, how many questions it has (the count of kept questions), and the anonymity promise in plain words - anonymous when the page flags that the questions need anonymity, otherwise exactly as planned (anonymous, confidential, or that answers carry names).questions- one object per question the page ties to a decision with an owner, in the draft's order, and no others:n(its draft number),text(the question, reworded only to remove hypothetical, leading or unbounded phrasing the page flags),type(rating,choiceoropen, as drafted),options(the answer choices for a rating or choice question - a 0-10 scale where the question names one, otherwise short choices of your design;[]for open).closing- one or two sentences: thanks, and what respondents will hear back, consistent with the page's close-the-loop pick (never promise a public post the page deletes).note- one sentence, at most 30 words, for the organizers: what to settle before fielding.
{
"task": "survey",
"title": "Feedback - an event",
"intro": "two or three sentences",
"questions": [ { "n": 1, "text": "a question", "type": "rating", "options": [ "0", "10" ] } ],
"closing": "one or two sentences",
"note": "one sentence"
}
The page requires exactly the kept questions in the draft order with their types and answer options, no hypothetical or leading wording left, the anonymity promise and the question count in the intro, and no public post promised where the page deletes it.
Worked examples - every lane, every sample
These are the page's three free samples (fictional events and people; every date is illustrative). 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. Every sample has both lanes.
Riverside Cloud Summit (a conference, the day after) - design
5 of 8 questions kept; depth pick core plus one role module; channel pick email plus reminders; back to respondents: you-said-we-changed note.
Input:
{
"task": "design",
"event": "Event: Riverside Cloud Summit 2027; conference of about 420 participants; last day 2027-04-16; already happened; recurring, compounding over several editions\nDay: a captive moment (closing plenary, demo day) yes; the run of show can still change no; mixed-level room no; run for hiring no; sponsors fund most of the budget no\nRoles: Attendees (400 reachable); Speakers (18 reachable); Sponsors (6 reachable); prior edition's response rate unknown\nPromises: anonymity anonymous; publication consent asked at collection no; private or NDA-bound no; public recap already planned yes",
"instrument": "Decisions waiting on the answers:\n D1. Hold a 2028 edition | owner Board chair Ana | due 2027-06-30\n D2. Keep the second track | owner Program chair Ben | due 2027-05-15\n D3. Renew the venue | owner Ops lead Chen | due 2027-05-01\n D4. Change the sponsor packages | owner Sponsor lead Dara | due 2027-06-01\nDraft questions, in order:\n Q1. How likely are you to recommend this event to a colleague (0-10)? | rating | asked of all | serves D1 (Hold a 2028 edition)\n Q2. Which sessions did you attend in the second track? | choice | asked of attendees | serves D2 (Keep the second track)\n Q3. Was there anything about the venue that made it harder to attend or to concentrate? | open | asked of attendees | serves D3 (Renew the venue)\n Q4. Would you attend a workshop day next year? | choice | asked of attendees | serves NO DECISION - UNOWNED\n Q5. Did you enjoy our amazing keynote lineup? | rating | asked of attendees | serves NO DECISION - UNOWNED\n Q6. Did the booth traffic match the goals you set for this sponsorship? | choice | asked of sponsors | serves D4 (Change the sponsor packages)\n Q7. How well does our audience fit your brand and business goals? | rating | asked of sponsors | serves D4 (Change the sponsor packages)\n Q8. Any other comments? | open | asked of all | serves NO DECISION - UNOWNED\nSend plan: channel Email plus reminders plus an incentive; first send 2027-04-22 (6 days after); reminders 2027-04-25, 2027-04-28, 2027-05-02\nBack to respondents as planned: Public findings post",
"facts": "Depth: the draft is module (Core plus one role module, 8 questions, 2 open); the page's pick is module (a decision owned by one role)\nChannel rungs: Single email available; Email plus reminders default; Email plus reminders plus an incentive deleted; In-room capture deleted; the page's pick is reminders; planned incentive\nMeasured bands: emailed 10-30% response; in-room 40-60% response (in-room completion 85-95% is a different quantity); an incentive lifts emailed response by about half again; two- or three-question surveys more than double the median response of seven-plus\nAnonymity: promised anonymous; sensitive questions none\nResponse rate per role: not fielded yet\nBack to respondents: planned public; the page's pick is note; rungs Nothing back available; Acknowledgement available; You-said-we-changed note default; Public findings post deleted\nGates: Zero unowned questions FAILED; Anonymity consistent with the questions and the channel FAILED; Sent within 48 hours of the event, or captured in the room FAILED; Response rate at or above the published floor, per role pending\nSignals to record: Response rate per role, never one blended number; Return intent - under about two-thirds is an early warning; Whether and how fast respondents heard back\nFindings (13):\n[fix] unowned (instrument): 3 questions with no named decision and owner: \"Would you attend a workshop day next year?\"; \"Did you enjoy our amazing keynote lineup?\"; \"Any other comments?\" - attach an owner or cut the question.\n[check] hypothetical (phrasing): Hypothetical: \"Would you attend a workshop day next year?\" - ask what people did, not what they would do.\n[check] leading (phrasing): Leading: \"Did you enjoy our amazing keynote lineup?\" - never signal the answer you want.\n[check] vague_box (phrasing): An unbounded box (\"Any other comments?\") collects nothing or an essay - ask for the one thing to change.\n[check] length (instrument): 8 questions - surveys of seven or more questions reach well under half the median response of a two- or three-question survey. Cut every question with no decision behind it.\n[info] promise (anonymity): Say in the instrument which promise this is: anonymous - nobody, the team included, can link an answer to a person.\n[info] free_text (anonymity): Free text will name people: decide who reads raw answers; anything about a speaker goes through the speaker-experience pass-through rule, and any code-of-conduct allegation leaves for that pipeline at once.\n[fix] channel_deleted (channel): Email plus reminders plus an incentive is deleted here - a prize draw needs an identity and anonymity forbids one.\n[info] next_edition (channel): Next edition, put a feedback line in the run of show at the captive moment - in-room capture runs at roughly two to four times the emailed rate.\n[check] send_slow (timing): Sent 6 days after the event - the practitioner consensus is 24-48 hours.\n[check] too_many_reminders (timing): 3 reminders - at most two; beyond that it is fatigue, not responses.\n[fix] loop_deleted (close the loop): A public findings post is deleted here - publication consent was not asked at collection time, and a quote cannot be retro-consented.\n[check] gate (readiness): Gates: 0 of 3 before fielding (the response-rate gate is checked after) - failing: zero unowned questions; anonymity consistent with the questions and the channel; sent within 48 hours of the event, or captured in the room."
}
Reply:
{
"task": "design",
"summary": "This is a module-depth survey that should ship as email plus reminders, not the planned incentive, and the first fix is cutting the 3 unowned draft questions.",
"depth": {
"pick": "module",
"draft": "module",
"note": "The draft makes a core plus one role module of 8 questions with 2 open, matching the page's pick for a decision owned by one role."
},
"channel": {
"pick": "reminders",
"planned": "incentive",
"note": "The page's pick is email plus reminders, but the planned incentive is deleted because a prize draw needs an identity that anonymity forbids."
},
"questions": [
{
"n": 1,
"keep": true,
"decision": "Hold a 2028 edition",
"note": "Keep as is; ties to a named decision and owner."
},
{
"n": 2,
"keep": true,
"decision": "Keep the second track",
"note": "Keep as is; ties to a named decision and owner."
},
{
"n": 3,
"keep": true,
"decision": "Renew the venue",
"note": "Keep as is; ties to a named decision and owner."
},
{
"n": 4,
"keep": false,
"decision": "",
"note": "Cut: unowned, and it asks what people would hypothetically do rather than what they did."
},
{
"n": 5,
"keep": false,
"decision": "",
"note": "Cut: unowned, and it signals the answer wanted rather than asking neutrally."
},
{
"n": 6,
"keep": true,
"decision": "Change the sponsor packages",
"note": "Keep as is; ties to a named decision and owner."
},
{
"n": 7,
"keep": true,
"decision": "Change the sponsor packages",
"note": "Keep as is; ties to a named decision and owner."
},
{
"n": 8,
"keep": false,
"decision": "",
"note": "Cut: unowned, and an unbounded comments box collects nothing or an essay."
}
],
"anonymity": {
"promise": "anonymous",
"note": "None of the kept questions touch judging, mentors or a named person, but the instrument must state plainly that no one, including the team, can link an answer to a respondent."
},
"timing": {
"send": "2027-04-22",
"reminders": [
"2027-04-25",
"2027-04-28",
"2027-05-02"
],
"followup": "",
"note": "The first send went out 6 days after the event, past the 24-48 hour window, and 3 reminders exceed the two-reminder cap that risks fatigue instead of responses."
},
"loop": {
"pick": "note",
"planned": "public",
"note": "The planned public findings post is deleted because publication consent was not asked at collection, and a quote cannot be retro-consented, so default to a you-said-we-changed note."
},
"rates": [],
"signals": [
"Response rate per role, never blended - a role far under its floor next edition would shift that role's budget toward in-room capture.",
"Return intent under about two-thirds - an early warning that would trigger a program review before the next edition is planned.",
"How fast respondents hear back - a slow turnaround next edition would move to an automatic acknowledgement sent soon after close."
],
"caveats": [
"3 questions have no named decision and owner - \"Would you attend a workshop day next year?\", \"Did you enjoy our amazing keynote lineup?\" and \"Any other comments?\" - attach an owner or cut each.",
"The planned incentive is deleted because a prize draw needs an identity and anonymity forbids one.",
"The planned public findings post is deleted because publication consent was not asked at collection and cannot be retro-consented.",
"Gates: 0 of 3 pass before fielding - unowned questions, anonymity versus channel, and the 48-hour send window all fail."
]
}
The page's check: kept - nothing to flag
Riverside Cloud Summit (a conference, the day after) - survey
5 of 8 questions kept; depth pick core plus one role module; channel pick email plus reminders; back to respondents: you-said-we-changed note.
Input:
{
"task": "survey",
"event": "Event: Riverside Cloud Summit 2027; conference of about 420 participants; last day 2027-04-16; already happened; recurring, compounding over several editions\nDay: a captive moment (closing plenary, demo day) yes; the run of show can still change no; mixed-level room no; run for hiring no; sponsors fund most of the budget no\nRoles: Attendees (400 reachable); Speakers (18 reachable); Sponsors (6 reachable); prior edition's response rate unknown\nPromises: anonymity anonymous; publication consent asked at collection no; private or NDA-bound no; public recap already planned yes",
"instrument": "Decisions waiting on the answers:\n D1. Hold a 2028 edition | owner Board chair Ana | due 2027-06-30\n D2. Keep the second track | owner Program chair Ben | due 2027-05-15\n D3. Renew the venue | owner Ops lead Chen | due 2027-05-01\n D4. Change the sponsor packages | owner Sponsor lead Dara | due 2027-06-01\nDraft questions, in order:\n Q1. How likely are you to recommend this event to a colleague (0-10)? | rating | asked of all | serves D1 (Hold a 2028 edition)\n Q2. Which sessions did you attend in the second track? | choice | asked of attendees | serves D2 (Keep the second track)\n Q3. Was there anything about the venue that made it harder to attend or to concentrate? | open | asked of attendees | serves D3 (Renew the venue)\n Q4. Would you attend a workshop day next year? | choice | asked of attendees | serves NO DECISION - UNOWNED\n Q5. Did you enjoy our amazing keynote lineup? | rating | asked of attendees | serves NO DECISION - UNOWNED\n Q6. Did the booth traffic match the goals you set for this sponsorship? | choice | asked of sponsors | serves D4 (Change the sponsor packages)\n Q7. How well does our audience fit your brand and business goals? | rating | asked of sponsors | serves D4 (Change the sponsor packages)\n Q8. Any other comments? | open | asked of all | serves NO DECISION - UNOWNED\nSend plan: channel Email plus reminders plus an incentive; first send 2027-04-22 (6 days after); reminders 2027-04-25, 2027-04-28, 2027-05-02\nBack to respondents as planned: Public findings post",
"facts": "Depth: the draft is module (Core plus one role module, 8 questions, 2 open); the page's pick is module (a decision owned by one role)\nChannel rungs: Single email available; Email plus reminders default; Email plus reminders plus an incentive deleted; In-room capture deleted; the page's pick is reminders; planned incentive\nMeasured bands: emailed 10-30% response; in-room 40-60% response (in-room completion 85-95% is a different quantity); an incentive lifts emailed response by about half again; two- or three-question surveys more than double the median response of seven-plus\nAnonymity: promised anonymous; sensitive questions none\nResponse rate per role: not fielded yet\nBack to respondents: planned public; the page's pick is note; rungs Nothing back available; Acknowledgement available; You-said-we-changed note default; Public findings post deleted\nGates: Zero unowned questions FAILED; Anonymity consistent with the questions and the channel FAILED; Sent within 48 hours of the event, or captured in the room FAILED; Response rate at or above the published floor, per role pending\nSignals to record: Response rate per role, never one blended number; Return intent - under about two-thirds is an early warning; Whether and how fast respondents heard back\nFindings (13):\n[fix] unowned (instrument): 3 questions with no named decision and owner: \"Would you attend a workshop day next year?\"; \"Did you enjoy our amazing keynote lineup?\"; \"Any other comments?\" - attach an owner or cut the question.\n[check] hypothetical (phrasing): Hypothetical: \"Would you attend a workshop day next year?\" - ask what people did, not what they would do.\n[check] leading (phrasing): Leading: \"Did you enjoy our amazing keynote lineup?\" - never signal the answer you want.\n[check] vague_box (phrasing): An unbounded box (\"Any other comments?\") collects nothing or an essay - ask for the one thing to change.\n[check] length (instrument): 8 questions - surveys of seven or more questions reach well under half the median response of a two- or three-question survey. Cut every question with no decision behind it.\n[info] promise (anonymity): Say in the instrument which promise this is: anonymous - nobody, the team included, can link an answer to a person.\n[info] free_text (anonymity): Free text will name people: decide who reads raw answers; anything about a speaker goes through the speaker-experience pass-through rule, and any code-of-conduct allegation leaves for that pipeline at once.\n[fix] channel_deleted (channel): Email plus reminders plus an incentive is deleted here - a prize draw needs an identity and anonymity forbids one.\n[info] next_edition (channel): Next edition, put a feedback line in the run of show at the captive moment - in-room capture runs at roughly two to four times the emailed rate.\n[check] send_slow (timing): Sent 6 days after the event - the practitioner consensus is 24-48 hours.\n[check] too_many_reminders (timing): 3 reminders - at most two; beyond that it is fatigue, not responses.\n[fix] loop_deleted (close the loop): A public findings post is deleted here - publication consent was not asked at collection time, and a quote cannot be retro-consented.\n[check] gate (readiness): Gates: 0 of 3 before fielding (the response-rate gate is checked after) - failing: zero unowned questions; anonymity consistent with the questions and the channel; sent within 48 hours of the event, or captured in the room."
}
Reply:
{
"task": "survey",
"title": "Feedback - Riverside Cloud Summit 2027",
"intro": "Riverside Cloud Summit 2027 has ended, and your answers will help decide whether we hold a 2028 edition, keep the second track, renew the venue, and shape next year's sponsor packages. This survey has 5 questions. Your answers are anonymous: no one, including the organizing team, can link a response back to you.",
"questions": [
{
"n": 1,
"text": "How likely are you to recommend this event to a colleague (0-10)?",
"type": "rating",
"options": [
"0",
"10"
]
},
{
"n": 2,
"text": "Which sessions did you attend in the second track?",
"type": "choice",
"options": [
"Session 1",
"Session 2",
"Session 3",
"I did not attend the second track"
]
},
{
"n": 3,
"text": "Was there anything about the venue that made it harder to attend or to concentrate?",
"type": "open",
"options": []
},
{
"n": 6,
"text": "Did the booth traffic match the goals you set for this sponsorship?",
"type": "choice",
"options": [
"Yes, met or exceeded our goals",
"Partially met our goals",
"No, did not meet our goals"
]
},
{
"n": 7,
"text": "How well does our audience fit your brand and business goals?",
"type": "rating",
"options": [
"Poor fit",
"Fair fit",
"Good fit",
"Excellent fit"
]
}
],
"closing": "Thank you for your feedback. We'll follow up with a note on what we changed as a result of what you told us.",
"note": "Drop the planned incentive before fielding, since a prize draw needs identities and the promise here is anonymous, and send at most two reminders, not three."
}
The page's check: kept - nothing to flag
Northgate Hack Weekend (a hackathon still being planned) - design
6 of 6 questions kept; depth pick full role-segmented instrument; channel pick in-room capture; back to respondents: you-said-we-changed note.
Input:
{
"task": "design",
"event": "Event: Northgate Hack Weekend; hackathon of about 150 participants; last day 2027-03-07; the day is still being planned; recurring\nDay: a captive moment (closing plenary, demo day) yes; the run of show can still change yes; mixed-level room no; run for hiring no; sponsors fund most of the budget no\nRoles: Hackers (150 reachable); Mentors (12 reachable); Judges (5 reachable); prior edition's response rate unknown\nPromises: anonymity named; publication consent asked at collection yes; private or NDA-bound no; public recap already planned no",
"instrument": "Decisions waiting on the answers:\n D1. Keep the team-matching session | owner Lead organizer Kai | due 2027-04-01\n D2. Mentor shift plan | owner Mentor lead Ira | due 2027-04-15\n D3. Judging rubric for next edition | owner Judging chair Sam | due 2027-05-01\n D4. Sponsor API track | owner Sponsor lead Lee | due 2027-04-20\nDraft questions, in order:\n Q1. How easily did your team form? | choice | asked of hackers | serves D1 (Keep the team-matching session)\n Q2. When you needed a mentor how long did you wait? | choice | asked of hackers | serves D2 (Mentor shift plan) | sensitive\n Q3. Were the judging criteria clear and applied fairly to your team? | rating | asked of hackers | serves D3 (Judging rubric for next edition) | sensitive\n Q4. How useful was the sponsor API documentation? | rating | asked of hackers | serves D4 (Sponsor API track)\n Q5. Did hackers bring you problems you could help with? | choice | asked of mentors | serves D2 (Mentor shift plan)\n Q6. What is the one thing we should change for next edition? | open | asked of all | serves D1 (Keep the team-matching session)\nSend plan: channel In-room capture; captured in the room; follow-up survey not planned\nBack to respondents as planned: You-said-we-changed note",
"facts": "Depth: the draft is full (Full role-segmented instrument, 6 questions, 1 open); the page's pick is full (a hackathon - its team, mentor, judging and tooling questions exist nowhere else)\nChannel rungs: Single email available; Email plus reminders default; Email plus reminders plus an incentive starved; In-room capture promoted; the page's pick is inroom; planned inroom\nMeasured bands: emailed 10-30% response; in-room 40-60% response (in-room completion 85-95% is a different quantity); an incentive lifts emailed response by about half again; two- or three-question surveys more than double the median response of seven-plus\nAnonymity: promised named; sensitive questions Q2, Q3\nResponse rate per role: not fielded yet\nBack to respondents: planned note; the page's pick is note; rungs Nothing back available; Acknowledgement available; You-said-we-changed note default; Public findings post starved\nGates: Zero unowned questions passed; Anonymity consistent with the questions and the channel FAILED; Sent within 48 hours of the event, or captured in the room passed; Response rate at or above the published floor, per role pending\nSignals to record: Response rate per role, never one blended number; Return intent - under about two-thirds is an early warning; Whether and how fast respondents heard back\nFindings (6):\n[check] hack_layer (instrument): The hackathon layer is missing continuation - a conference core alone measures the catering.\n[fix] no_follow (timing): No follow-up survey planned - ask whether teams kept working one to two weeks after the event, as a second short survey.\n[fix] anon_needed (anonymity): Questions about judging, mentors, inclusion or a named person (\"When you needed a mentor how long did you wait?\"; \"Were the judging criteria clear and applied...\") with named responses - a respondent who suspects the subject will read it gives an unusable rating. Turn anonymity on.\n[info] free_text (anonymity): Free text will name people: decide who reads raw answers; anything about a speaker goes through the speaker-experience pass-through rule, and any code-of-conduct allegation leaves for that pipeline at once.\n[info] inroom (channel): In-room capture is promoted: a code at the captive moment, answered while the session is still fresh. It needs one line in the run of show.\n[check] gate (readiness): Gates: 2 of 3 before fielding (the response-rate gate is checked after) - failing: anonymity consistent with the questions and the channel."
}
Reply:
{
"task": "design",
"summary": "This full role-segmented hackathon survey is captured in the room, but anonymity must be turned on since the judging and mentor questions are sensitive and currently answered by name.",
"depth": {
"pick": "full",
"draft": "full",
"note": "The draft makes the full role-segmented instrument of 6 questions with 1 open, matching the page's pick for a hackathon whose team, mentor and judging questions exist nowhere else."
},
"channel": {
"pick": "inroom",
"planned": "inroom",
"note": "The page promotes in-room capture because the day is still being planned and a captive moment exists, and it needs one line added to the run of show."
},
"questions": [
{
"n": 1,
"keep": true,
"decision": "Keep the team-matching session",
"note": "Keep as is; ties to a named decision and owner."
},
{
"n": 2,
"keep": true,
"decision": "Mentor shift plan",
"note": "Keep, but anonymize - it is sensitive and currently answered under a named promise."
},
{
"n": 3,
"keep": true,
"decision": "Judging rubric for next edition",
"note": "Keep, but anonymize - a respondent who suspects the subject will read a named answer gives an unusable rating."
},
{
"n": 4,
"keep": true,
"decision": "Sponsor API track",
"note": "Keep as is; ties to a named decision and owner."
},
{
"n": 5,
"keep": true,
"decision": "Mentor shift plan",
"note": "Keep as is; ties to a named decision and owner."
},
{
"n": 6,
"keep": true,
"decision": "Keep the team-matching session",
"note": "Keep as is; already asks for the one thing to change rather than an unbounded box."
}
],
"anonymity": {
"promise": "named",
"note": "The judging-fairness and mentor-wait questions need anonymity, since a respondent who suspects the subject will read a named answer gives an unusable rating, so the promise should move from named to anonymous before fielding."
},
"timing": {
"send": "",
"reminders": [],
"followup": "",
"note": "No follow-up survey is planned, but hackathon practice calls for a second short survey one to two weeks after the event to check whether teams kept working."
},
"loop": {
"pick": "note",
"planned": "note",
"note": "The planned you-said-we-changed note matches the page's pick, while a public findings post stays off the table since no public recap is planned."
},
"rates": [],
"signals": [
"Response rate per role, never blended - if mentors or judges fall short next edition, their module would move from in-room to a direct ask.",
"Return intent under about two-thirds - an early warning that would move the team-matching session earlier in the day next edition.",
"How fast respondents hear back - a slow turnaround next edition would push the you-said-we-changed note out before the follow-up survey closes."
],
"caveats": [
"Turn anonymity on - the judging-fairness and mentor-wait questions are sensitive, and a respondent who suspects the subject will read a named answer gives an unusable rating.",
"No follow-up survey is planned - add a second short survey one to two weeks after the event asking whether teams kept working.",
"The hackathon layer is missing continuation - a conference core alone measures the catering.",
"Gates: 2 of 3 pass before fielding - anonymity consistent with the questions and the channel is the one failing."
]
}
The page's check: kept - nothing to flag
Northgate Hack Weekend (a hackathon still being planned) - survey
6 of 6 questions kept; depth pick full role-segmented instrument; channel pick in-room capture; back to respondents: you-said-we-changed note.
Input:
{
"task": "survey",
"event": "Event: Northgate Hack Weekend; hackathon of about 150 participants; last day 2027-03-07; the day is still being planned; recurring\nDay: a captive moment (closing plenary, demo day) yes; the run of show can still change yes; mixed-level room no; run for hiring no; sponsors fund most of the budget no\nRoles: Hackers (150 reachable); Mentors (12 reachable); Judges (5 reachable); prior edition's response rate unknown\nPromises: anonymity named; publication consent asked at collection yes; private or NDA-bound no; public recap already planned no",
"instrument": "Decisions waiting on the answers:\n D1. Keep the team-matching session | owner Lead organizer Kai | due 2027-04-01\n D2. Mentor shift plan | owner Mentor lead Ira | due 2027-04-15\n D3. Judging rubric for next edition | owner Judging chair Sam | due 2027-05-01\n D4. Sponsor API track | owner Sponsor lead Lee | due 2027-04-20\nDraft questions, in order:\n Q1. How easily did your team form? | choice | asked of hackers | serves D1 (Keep the team-matching session)\n Q2. When you needed a mentor how long did you wait? | choice | asked of hackers | serves D2 (Mentor shift plan) | sensitive\n Q3. Were the judging criteria clear and applied fairly to your team? | rating | asked of hackers | serves D3 (Judging rubric for next edition) | sensitive\n Q4. How useful was the sponsor API documentation? | rating | asked of hackers | serves D4 (Sponsor API track)\n Q5. Did hackers bring you problems you could help with? | choice | asked of mentors | serves D2 (Mentor shift plan)\n Q6. What is the one thing we should change for next edition? | open | asked of all | serves D1 (Keep the team-matching session)\nSend plan: channel In-room capture; captured in the room; follow-up survey not planned\nBack to respondents as planned: You-said-we-changed note",
"facts": "Depth: the draft is full (Full role-segmented instrument, 6 questions, 1 open); the page's pick is full (a hackathon - its team, mentor, judging and tooling questions exist nowhere else)\nChannel rungs: Single email available; Email plus reminders default; Email plus reminders plus an incentive starved; In-room capture promoted; the page's pick is inroom; planned inroom\nMeasured bands: emailed 10-30% response; in-room 40-60% response (in-room completion 85-95% is a different quantity); an incentive lifts emailed response by about half again; two- or three-question surveys more than double the median response of seven-plus\nAnonymity: promised named; sensitive questions Q2, Q3\nResponse rate per role: not fielded yet\nBack to respondents: planned note; the page's pick is note; rungs Nothing back available; Acknowledgement available; You-said-we-changed note default; Public findings post starved\nGates: Zero unowned questions passed; Anonymity consistent with the questions and the channel FAILED; Sent within 48 hours of the event, or captured in the room passed; Response rate at or above the published floor, per role pending\nSignals to record: Response rate per role, never one blended number; Return intent - under about two-thirds is an early warning; Whether and how fast respondents heard back\nFindings (6):\n[check] hack_layer (instrument): The hackathon layer is missing continuation - a conference core alone measures the catering.\n[fix] no_follow (timing): No follow-up survey planned - ask whether teams kept working one to two weeks after the event, as a second short survey.\n[fix] anon_needed (anonymity): Questions about judging, mentors, inclusion or a named person (\"When you needed a mentor how long did you wait?\"; \"Were the judging criteria clear and applied...\") with named responses - a respondent who suspects the subject will read it gives an unusable rating. Turn anonymity on.\n[info] free_text (anonymity): Free text will name people: decide who reads raw answers; anything about a speaker goes through the speaker-experience pass-through rule, and any code-of-conduct allegation leaves for that pipeline at once.\n[info] inroom (channel): In-room capture is promoted: a code at the captive moment, answered while the session is still fresh. It needs one line in the run of show.\n[check] gate (readiness): Gates: 2 of 3 before fielding (the response-rate gate is checked after) - failing: anonymity consistent with the questions and the channel."
}
Reply:
{
"task": "survey",
"title": "Feedback - Northgate Hack Weekend",
"intro": "This short survey will shape decisions about team formation, mentor shifts, the judging rubric and the sponsor API track for the next Northgate Hack Weekend. It has six questions, and your answers are anonymous.",
"questions": [
{
"n": 1,
"text": "How easily did your team form?",
"type": "choice",
"options": [
"Very easily",
"Fairly easily",
"With difficulty",
"We did not form a team"
]
},
{
"n": 2,
"text": "When you needed a mentor how long did you wait?",
"type": "choice",
"options": [
"Did not need a mentor",
"No wait",
"A short wait",
"A long wait"
]
},
{
"n": 3,
"text": "Were the judging criteria clear and applied fairly to your team?",
"type": "rating",
"options": [
"Not at all",
"Somewhat",
"Mostly",
"Completely"
]
},
{
"n": 4,
"text": "How useful was the sponsor API documentation?",
"type": "rating",
"options": [
"Not useful",
"Somewhat useful",
"Useful",
"Very useful"
]
},
{
"n": 5,
"text": "Did hackers bring you problems you could help with?",
"type": "choice",
"options": [
"Yes, often",
"Yes, sometimes",
"Rarely",
"No"
]
},
{
"n": 6,
"text": "What is the one thing we should change for next edition?",
"type": "open",
"options": []
}
],
"closing": "Thank you for your feedback. We will follow up with what changed for the next edition based on what you tell us.",
"note": "Settle how in-room capture stays anonymous before fielding, since the plan currently names respondents but the judging and mentor questions need anonymity."
}
The page's check: kept - nothing to flag
Spree Rust Evenings (a meetup survey already fielded) - design
2 of 2 questions kept; depth pick single-question pulse; channel pick email plus reminders; back to respondents: you-said-we-changed note.
Input:
{
"task": "design",
"event": "Event: Spree Rust Evenings #42; meetup of about 45 participants; last day 2027-02-11; already happened; recurring\nDay: a captive moment (closing plenary, demo day) no; the run of show can still change no; mixed-level room yes; run for hiring no; sponsors fund most of the budget no\nRoles: Attendees (45 reachable, 3 answered); prior edition's response rate unknown\nPromises: anonymity anonymous; publication consent asked at collection no; private or NDA-bound no; public recap already planned no",
"instrument": "Decisions waiting on the answers:\n D1. Keep the Thursday evening slot | owner Organizer Mia | due 2027-03-01\nDraft questions, in order:\n Q1. How likely are you to recommend this meetup to a friend (0-10)? | rating | asked of all | serves D1 (Keep the Thursday evening slot)\n Q2. What is the one thing we should change? | open | asked of all | serves D1 (Keep the Thursday evening slot)\nSend plan: channel Email plus reminders; first send 2027-02-12 (1 days after); reminders 2027-02-15\nBack to respondents as planned: Nothing back",
"facts": "Depth: the draft is pulse (Single-question pulse, 2 questions, 1 open); the page's pick is pulse (a meetup of a few dozen); role modules and the full instrument are deleted - only attendees exist\nChannel rungs: Single email available; Email plus reminders default; Email plus reminders plus an incentive deleted; In-room capture deleted; the page's pick is reminders; planned reminders\nMeasured bands: emailed 10-30% response; in-room 40-60% response (in-room completion 85-95% is a different quantity); an incentive lifts emailed response by about half again; two- or three-question surveys more than double the median response of seven-plus\nAnonymity: promised anonymous; sensitive questions none\nResponse rate per role: Attendees 3 of 45 = 6.7% (floor 10%)\nBack to respondents: planned nothing; the page's pick is note; rungs Nothing back available; Acknowledgement available; You-said-we-changed note default; Public findings post deleted\nGates: Zero unowned questions passed; Anonymity consistent with the questions and the channel passed; Sent within 48 hours of the event, or captured in the room passed; Response rate at or above the published floor, per role FAILED\nSignals to record: Response rate per role, never one blended number; Return intent - under about two-thirds is an early warning; Whether and how fast respondents heard back\nFindings (7):\n[check] level (instrument): A mixed-level room: add a closed level question (too basic / about right / too advanced) and the respondent's own level, and cross the two before reporting.\n[info] automate (channel): Send the meetup pulse automatically after every event - same instrument, same delay, so one meetup compares with the next.\n[info] promise (anonymity): Say in the instrument which promise this is: anonymous - nobody, the team included, can link an answer to a person.\n[info] free_text (anonymity): Free text will name people: decide who reads raw answers; anything about a speaker goes through the speaker-experience pass-through rule, and any code-of-conduct allegation leaves for that pipeline at once.\n[fix] below_floor (response): Attendees: 6.7% answered, under the 10% floor for emailed surveys - the collection failed, not the audience. Send the reminder, shorten the instrument, or move the channel.\n[check] loop_nothing (close the loop): Nothing goes back to respondents - the failure that compounds: people who hear nothing do not answer next year.\n[check] gate (readiness): Gates: 3 of 4 - failing: response rate at or above the published floor, per role."
}
Reply:
{
"task": "design",
"summary": "This pulse-depth meetup survey used email plus reminders but fell to 6.7% response against the 10% floor, so shorten the instrument or add a reminder before the next edition.",
"depth": {
"pick": "pulse",
"draft": "pulse",
"note": "The draft makes a single-question pulse of 2 questions with 1 open, matching the page's pick for a meetup of a few dozen, with role modules and the full instrument deleted."
},
"channel": {
"pick": "reminders",
"planned": "reminders",
"note": "The incentive rung is deleted because anonymity forbids a draw needing identity, and in-room capture is deleted now that the event has already happened."
},
"questions": [
{
"n": 1,
"keep": true,
"decision": "Keep the Thursday evening slot",
"note": "Keep as is; ties to a named decision and owner."
},
{
"n": 2,
"keep": true,
"decision": "Keep the Thursday evening slot",
"note": "Keep as is; already asks for the one thing to change rather than an unbounded box."
}
],
"anonymity": {
"promise": "anonymous",
"note": "Neither kept question touches judging, mentors or a named person, but the instrument must still state that no one, including the team, can link an answer to a respondent."
},
"timing": {
"send": "2027-02-12",
"reminders": [
"2027-02-15"
],
"followup": "",
"note": "Nothing here needs fixing, though automating the pulse send at a fixed delay after every meetup would make this edition comparable to the next."
},
"loop": {
"pick": "note",
"planned": "nothing",
"note": "Nothing is currently planned to go back to respondents, but the default you-said-we-changed note matters here since people who hear nothing tend not to answer next year."
},
"rates": [
{
"role": "Attendees",
"rate": 6.7,
"note": "3 of 45 answered (6.7%), under the 10% floor - a collection-side shortfall, not an audience one."
}
],
"signals": [
"Response rate per role, never blended - since attendees alone sit at 6.7% against a 10% floor, next edition would shorten the instrument or add a second reminder.",
"Return intent under about two-thirds - an early warning that would prompt revisiting the Thursday evening slot before the next edition.",
"How fast respondents hear back - with nothing sent back this time, next edition would add a short acknowledgement."
],
"caveats": [
"Attendees: 3 of 45 answered (6.7%), under the 10% floor for emailed surveys - a collection-side shortfall, not an audience one; send a reminder, shorten the instrument, or move the channel.",
"The room is mixed-level - add a closed level question (too basic, about right, or too advanced) plus the respondent's own level, and cross the two before reporting.",
"Nothing currently goes back to respondents, which compounds: people who hear nothing do not answer next year.",
"Gates: 3 of 4 pass - the one failing is response rate at or above the published floor, per role."
]
}
The page's check: kept - nothing to flag
Spree Rust Evenings (a meetup survey already fielded) - survey
2 of 2 questions kept; depth pick single-question pulse; channel pick email plus reminders; back to respondents: you-said-we-changed note.
Input:
{
"task": "survey",
"event": "Event: Spree Rust Evenings #42; meetup of about 45 participants; last day 2027-02-11; already happened; recurring\nDay: a captive moment (closing plenary, demo day) no; the run of show can still change no; mixed-level room yes; run for hiring no; sponsors fund most of the budget no\nRoles: Attendees (45 reachable, 3 answered); prior edition's response rate unknown\nPromises: anonymity anonymous; publication consent asked at collection no; private or NDA-bound no; public recap already planned no",
"instrument": "Decisions waiting on the answers:\n D1. Keep the Thursday evening slot | owner Organizer Mia | due 2027-03-01\nDraft questions, in order:\n Q1. How likely are you to recommend this meetup to a friend (0-10)? | rating | asked of all | serves D1 (Keep the Thursday evening slot)\n Q2. What is the one thing we should change? | open | asked of all | serves D1 (Keep the Thursday evening slot)\nSend plan: channel Email plus reminders; first send 2027-02-12 (1 days after); reminders 2027-02-15\nBack to respondents as planned: Nothing back",
"facts": "Depth: the draft is pulse (Single-question pulse, 2 questions, 1 open); the page's pick is pulse (a meetup of a few dozen); role modules and the full instrument are deleted - only attendees exist\nChannel rungs: Single email available; Email plus reminders default; Email plus reminders plus an incentive deleted; In-room capture deleted; the page's pick is reminders; planned reminders\nMeasured bands: emailed 10-30% response; in-room 40-60% response (in-room completion 85-95% is a different quantity); an incentive lifts emailed response by about half again; two- or three-question surveys more than double the median response of seven-plus\nAnonymity: promised anonymous; sensitive questions none\nResponse rate per role: Attendees 3 of 45 = 6.7% (floor 10%)\nBack to respondents: planned nothing; the page's pick is note; rungs Nothing back available; Acknowledgement available; You-said-we-changed note default; Public findings post deleted\nGates: Zero unowned questions passed; Anonymity consistent with the questions and the channel passed; Sent within 48 hours of the event, or captured in the room passed; Response rate at or above the published floor, per role FAILED\nSignals to record: Response rate per role, never one blended number; Return intent - under about two-thirds is an early warning; Whether and how fast respondents heard back\nFindings (7):\n[check] level (instrument): A mixed-level room: add a closed level question (too basic / about right / too advanced) and the respondent's own level, and cross the two before reporting.\n[info] automate (channel): Send the meetup pulse automatically after every event - same instrument, same delay, so one meetup compares with the next.\n[info] promise (anonymity): Say in the instrument which promise this is: anonymous - nobody, the team included, can link an answer to a person.\n[info] free_text (anonymity): Free text will name people: decide who reads raw answers; anything about a speaker goes through the speaker-experience pass-through rule, and any code-of-conduct allegation leaves for that pipeline at once.\n[fix] below_floor (response): Attendees: 6.7% answered, under the 10% floor for emailed surveys - the collection failed, not the audience. Send the reminder, shorten the instrument, or move the channel.\n[check] loop_nothing (close the loop): Nothing goes back to respondents - the failure that compounds: people who hear nothing do not answer next year.\n[check] gate (readiness): Gates: 3 of 4 - failing: response rate at or above the published floor, per role."
}
Reply:
{
"task": "survey",
"title": "Feedback - Spree Rust Evenings #42",
"intro": "Spree Rust Evenings #42 has wrapped, and your answer will help decide whether we keep the Thursday evening slot. This survey has 2 questions. Your answers are anonymous: no one, including the organizers, can link a response back to you.",
"questions": [
{
"n": 1,
"text": "How likely are you to recommend this meetup to a friend (0-10)?",
"type": "rating",
"options": [
"0",
"10"
]
},
{
"n": 2,
"text": "What is the one thing we should change?",
"type": "open",
"options": []
}
],
"closing": "Thanks for sharing your feedback. We'll follow up with a short note on what we're changing based on what you told us.",
"note": "Response sits at 6.7%, under the 10% floor - send the planned reminder, shorten the instrument further, or move the channel before closing collection."
}
The page's check: kept - nothing to flag
The questions CSV for the first sample
n,question,type,asked_of,decision,owner,owned,sensitive
1,How likely are you to recommend this event to a colleague (0-10)?,rating,all,Hold a 2028 edition,Board chair Ana,yes,
2,Which sessions did you attend in the second track?,choice,attendees,Keep the second track,Program chair Ben,yes,
3,Was there anything about the venue that made it harder to attend or to concentrate?,open,attendees,Renew the venue,Ops lead Chen,yes,
4,Would you attend a workshop day next year?,choice,attendees,,,no,
5,Did you enjoy our amazing keynote lineup?,rating,attendees,,,no,
6,Did the booth traffic match the goals you set for this sponsorship?,choice,sponsors,Change the sponsor packages,Sponsor lead Dara,yes,
7,How well does our audience fit your brand and business goals?,rating,sponsors,Change the sponsor packages,Sponsor lead Dara,yes,
8,Any other comments?,open,all,,,no,
Build the input yourself
Load the engine in node (global.window = global; require("./surveykit.js"); require("./surveyreply.js")), then:
const K = SurveyKit, R = SurveyReply;
const f = K.form({ name: "My Conf", format: "conference", happened: "yes", eventEnd: "2027-04-16", sendDate: "2027-04-17", anon: "anonymous",
roles: fs.readFileSync("roles.csv", "utf8"), decisions: fs.readFileSync("decisions.csv", "utf8"), questions: fs.readFileSync("questions.csv", "utf8") });
// f.qs (owned, dec, sensitive), f.unowned, f.depth, f.recDepth, f.rungs, f.recChannel, f.sendDays, f.rems
// f.rateRows, f.floor, f.loopRungs, f.recLoop, f.gate, f.findings
K.questionsCSV(f) // n, question, type, asked_of, decision, owner, owned, sensitive
const b = K.buildInput(f, "design"); // or "survey"
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "design");
// 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://survey-after.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://survey-after.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"]
// survey-after.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://survey-after.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://survey-after.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
}
// SurveyAfter.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 SurveyAfter {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://survey-after.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://survey-after.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://survey-after.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://survey-after.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":"survey-after"}' | 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": "survey-after"})["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: "survey-after" })).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": "survey-after"}, 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", "survey-after"), 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: "survey-after" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "survey-after"])["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 = "survey-after" }))!["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": "design", "event": "Event: ...; conference of about 420 participants; last day 2027-04-16 ...", "instrument": "Decisions waiting on the answers: ...", "facts": "Depth: the draft is module ..."}
# - 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": "design", "event": "Event: ...; conference of about 420 participants; last day 2027-04-16 ...", "instrument": "Decisions waiting on the answers: ...", "facts": "Depth: the draft is module ..."}
# - 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: "design" or "survey", event, instrument, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses survey-after:<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 design memo carries summary, depth, channel, questions, anonymity, timing, loop, rates, signals and caveats; a survey carries title, intro, questions, closing and note. The code below prints the headline parts of either (list items are plain strings).
KEY="survey-after:design:$(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, .note // .intro, ((.signals // .caveats)[] | "\(.signal // .caveat): \(.text // .text)")' reply.json
jq -r '.closing // .note // empty' reply.json # the closing line (survey)
import hashlib, time
key = "survey-after:" + 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("summary"))
print(reply.get("note") or reply.get("intro", ""))
for s in reply.get("signals") or reply.get("caveats") or []:
print(s.get("signal", s.get("caveat")), "-", s.get("text", s.get("text")))
if reply.get("closing"):
print("closing:", reply["closing"])
import { createHash } from "node:crypto";
const key = `survey-after:${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.note ?? reply.intro);
for (const s of reply.signals || reply.caveats || []) console.log(s.signal ?? s.caveat, "-", s.text ?? s.text);
if (reply.closing) console.log("closing:", reply.closing);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "survey-after:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
items, _ := reply["signals"].([]any) // signals and caveats are plain strings
fmt.Println(j["charged_credits"], j["truncated"], reply["summary"])
fmt.Println(reply["note"], reply["intro"])
for _, x := range items {
c := x.(map[string]any)
fmt.Println(c["signal"], "-", c["status"])
}
if fix, ok := reply["closing"].(string); ok && fix != "" {
fmt.Println("closing:", fix)
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "survey-after:" + 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.get("summary"));
System.out.println(reply.path("note").asText(reply.path("intro").asText()));
for (JsonNode s : reply.has("signals") ? reply.path("signals") : reply.path("caveats")) System.out.println(s.path("signal").asText(s.path("caveat").asText()) + " - " + s.path("status").asText(s.path("text").asText()));
if (!reply.path("closing").asText().isEmpty()) System.out.println("closing: " + reply.get("closing").asText());
require "digest"
key = "survey-after:#{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["summary"]].inspect
puts reply["note"] || reply["intro"]
(reply["signals"] || reply["caveats"] || []).each { |s| puts "#{s["signal"] || s["caveat"]} - #{s["text"] || s["text"]}" }
puts "closing: #{reply["closing"]}" unless reply["closing"].to_s.empty?
$key = "survey-after:" . $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["summary"] ?? ""), "\n";
echo $reply["note"] ?? $reply["intro"] ?? "", "\n";
foreach ($reply["signals"] ?? $reply["caveats"] ?? [] as $s) echo $s["signal"] ?? $s["caveat"], " - ", $s["text"] ?? $s["text"], "\n";
if (!empty($reply["closing"])) echo "closing: ", $reply["closing"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"survey-after:{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!["summary"]}");
Console.WriteLine($"{reply["note"] ?? reply["intro"]}");
foreach (var s in (reply["signals"] ?? reply["caveats"]) as JsonArray ?? new JsonArray()) Console.WriteLine($"{s!["signal"] ?? s["caveat"]} - {s["text"] ?? s["text"]}");
if (reply["closing"] is JsonNode fix && fix.GetValue<string>() != "") Console.WriteLine("closing: " + 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: survey-after:design:$(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": "design", "event": "Event: ...; conference of about 420 participants; last day 2027-04-16 ...", "instrument": "Decisions waiting on the answers: ...", "facts": "Depth: the draft is module ..."}
# - 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": "survey-after:design:" + 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": `survey-after:design:${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", "survey-after:design:"+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", "survey-after:design:" + 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" => "survey-after:design:#{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: survey-after:design:" . 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", $"survey-after:design:{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);