Drive Oven Relay from your own code
Both metered steps - planning the feast day and rescuing it from the clock - are available over HTTP. A natural use is a meal-planning app, a recipe box or a shared family checklist that already knows the menu: your code sends the menu and the kitchen, draws the timeline, and on the day asks for a rescue when something runs late.
The split first: the page does the mechanics; the model gives the judgement; the page checks it. The dish steps (estimates), the USDA whole-turkey roast and thaw times, the food-safety notes, the free draft timeline and its clash list come from relaykit.js, which runs in node as well as in the browser. Its check function then re-reads the model's timeline: no oven holds more dishes than it can, no two dishes share an oven more than 25 F apart, no more pots than burners or hands-on jobs than cooks, nothing before the earliest start or after serving. reconcile adds the rest: a "fits" verdict with clashes is flagged, every dish must be planned or cut, poultry needs its 165 F line, and a rescue must be a food-safety stop when the page reads a 2-hour-rule breach in the note.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"oven-relay"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field 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 tasks are metered and need a personal token. |
| 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
Every run sends task, which routes the one system prompt to one of two contracts:
| task | What it returns | Verdict field |
|---|---|---|
plan | A title, a summary, serve (HH:MM); tasks [{id, dish, label, res, start, end, temp_f, full, active, day, cite}] where res is oven1, oven2, mini, burner, microwave, slow_cooker, hands, counter or fridge and day is 0 for the feast day, -1 for the day before; moves (what changed from the draft); cut (what has to give); safety; questions (need-info only); caveats. | verdict: fits, tight, overbooked, need_info |
rescue | A title, a summary, the new serve; changes [{id, start, end, res?, text, cite}] moving plan steps; added new steps (R1…); dropped [{dish, text, cite}]; steps (the do-it-now list); tell_guests (one sentence); safety; caveats. | verdict: on_track, shift, cut, food_safety_stop |
A line is always {text, cite}. Cites are the page's ids: dishes D1 D2…, food-safety notes S1…, draft clashes X1…, timeline steps K1…, problem facts P1…, and E-event, I-kitchen, N-now. When the page reads perishable food out for 2 hours or more in the rescue note, it sets problem.floor to food_safety_stop and the rescue must answer that.
The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (RelayKit.toWire does this). task, event, kitchen, dishes and checks are required.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | plan or rescue. |
event | string (JSON) | {serve, earliest, date, day_label, guests, allow_make_ahead}; times are 24-hour HH:MM, and may be null. |
kitchen | string (JSON) | {ovens[{id, name, dishes_at_once}], burners, cooks, extras[]}. |
dishes | string (JSON) | [{id, text, name, library, known, hot, weight_lb, weight_assumed, stuffed, bought, can_make_ahead_days, wants_ahead, steps[{label, res, min, temp_f?, full?, active?}]}] from RelayKit.prescan. |
checks | string (JSON) | {safety[{id, level, text}], notes[], draft{tasks[], clashes[], could_not_place[], moved_ahead[]}}; a rescue sends no draft, so its K# ids always mean the plan being followed. |
plan | string (JSON) | rescue only: {serve, tasks[]} - the timeline being followed. |
now | string | rescue only: HH:MM. |
problem | string (JSON) | rescue only: {text, facts[], safety[], floor, done[]} from RelayKit.readProblem; long notes keep their start and end around a marker. |
note | string | Optional, the person's own words; over 600 characters keeps the start and the end. |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object. Parse it with RelayKit.parseReply, then normalize, then check it with reconcile(reply, {pre, draftX}) for a plan or reconcile(reply, {pre, plan, now, prob, done}) for a rescue. applyRescue(plan.tasks, rescue, now) gives the rescued timeline; check(tasks, pre) lists its clashes; toIcs and toCsv export it.
Worked examples
Replies produced against the app's prompt for the sample feasts (made-up menus and kitchens; lists shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons. Inputs are shown with the dishes and the draft folded.
task: plan - Classic, one oven (fits)
Input:
{
"task": "plan",
"event": "{\"serve\":\"16:00\",\"earliest\":\"08:00\",\"date\":\"2026-11-26\",\"day_label\":\"Thu Nov 26\",\"guests\":10,\"allow_make_ahead\":true}",
"kitchen": "{\"ovens\":[{\"id\":\"oven1\",\"name\":\"Oven\",\"dishes_at_once\":2}],\"burners\":4,\"cooks\":1,\"extras\":[\"microwave\"]}",
"dishes": "[{\"id\":\"D1\",\"text\":\"Turkey, 16 lb\",\"name\":\"Whole turkey\",\"library\":\"turkey\",\"known\":true,\"hot\":true,\"weight_lb\":16,\"weight_assumed\":false,\"stuffed\":false,\"bought\":false,\"can_make_ahead_days\":0,\"wants_ahead\":false,\"steps\":[{\"label\":\"Season and truss\",\"res\":\"hands\",\"min\":25,\"active\":true},{\"label\":\"Roast\",\"res\":\"oven\",\"min\":255,\"temp_f\":325,\"full\":true},{\"label\":\"Rest under foil\",\"res\":\"counter\",\"min\":35},{\"label\":\"Carve\",\"res\":\"hands\",\"min\":15,\"active\":true}]},\"... 7 more dishes\"]",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Whole turkey (D1) is done at 165 F in the thickest part of the thigh, the wing and the breast - check with a thermometer, not the colour (USDA).\"},{\"id\":\"S2\",\"level\":\"caution\",\"text\":\"If the 16 lb turkey is frozen, thaw it in the fridge for about 4 days - roughly a day per 4 to 5 lb (USDA) - so move it to the fridge by Sun Nov 22.\"},{\"id\":\"S3\",\"level\":\"caution\",\"text\":\"Get leftovers into the fridge within 2 hours of coming off the heat (USDA 2-hour rule).\"}],\"notes\":[],\"draft\":{\"tasks\":[{\"id\":\"K1\",\"dish\":\"D4\",\"label\":\"Assemble\",\"res\":\"hands\",\"start\":\"10:00\",\"end\":\"10:15\",\"day\":-1,\"active\":true},{\"id\":\"K2\",\"dish\":\"D8\",\"label\":\"Make the filling, fill the crust\",\"res\":\"hands\",\"start\":\"09:35\",\"end\":\"10:00\",\"day\":0,\"active\":true},\"... 17 more steps\"],\"clashes\":[{\"id\":\"X1\",\"level\":\"clash\",\"code\":\"oven_full\",\"text\":\"15:25-15:40: more in the oven than fits (2 dishes at once; a big roast fills it) - K13, K15, K16.\"},{\"id\":\"X2\",\"level\":\"clash\",\"code\":\"oven_full\",\"text\":\"15:40-15:55: more in the oven than fits (2 dishes at once; a big roast fills it) - K13, K15, K16, K18.\"}],\"could_not_place\":[\"D4\",\"D7\"],\"moved_ahead\":[\"D4\"]}}",
"note": "Just me cooking. My partner can carve if needed."
}
Reply:
{
"task": "plan",
"verdict": "fits",
"title": "Classic Thanksgiving for 10 - one oven, one cook",
"summary": "The turkey claims the oven from 10:30 to 14:45, then its 45-minute rest becomes the oven's busiest window: stuffing, sweet potato and green bean casseroles stagger through two at a time, with rolls sliding in last for 15:48. Pie is made, baked and cooled the day before, and the three casseroles are assembled the day before too, so day-of oven baking is all that is left. Carving 15 minutes early and holding the bird warm under foil gives the plan about 15 minutes of slack before 16:00.",
"serve": "16:00",
"tasks": [
{
"id": "K1",
"dish": "D8",
"label": "Make the filling, fill the crust",
"res": "hands",
"start": "09:00",
"end": "09:25",
"active": true,
"full": false,
"day": -1,
"cite": [
"D8"
]
},
{
"id": "K2",
"dish": "D8",
"label": "Bake",
"res": "oven1",
"start": "09:25",
"end": "10:20",
"temp_f": 375,
"active": false,
"full": false,
"day": -1,
"cite": [
"D8"
]
},
{
"id": "K3",
"dish": "D2",
"label": "Chop, saute and mix",
"res": "hands",
"start": "09:25",
"end": "09:50",
"active": true,
"full": false,
"day": -1,
"cite": [
"D2"
]
},
"..."
],
"moves": [
{
"text": "Moved the pie entirely to the day before - filled, baked and cooled on day -1 - instead of baking it day-of as the draft did. It's a cold dish anyway, and it frees the morning oven slot the turkey chain needs.",
"cite": [
"D8"
]
},
{
"text": "Assembled the stuffing, green bean casserole and sweet potato casserole the day before (chop/saute/mix, boil/mash, assemble) and refrigerated them, so day 0 only needs the actual baking.",
"cite": [
"D2",
"D4",
"D5"
]
},
{
"text": "Fixed the 15:25-15:55 oven_full pile-up by staggering the four post-turkey bakes into two lanes instead of trying to run all of them at once: stuffing then rolls in one lane, sweet potato then green beans in the other, so at most 2 dishes are ever in the oven together. D4 and D7, which the draft couldn't place, both land cleanly now.",
"cite": [
"X1",
"X2",
"D2",
"D4",
"D5",
"D7"
]
},
"..."
],
"cut": [],
"safety": [
{
"text": "Check the turkey (D1) with an instant-read thermometer in the thickest part of the thigh, the wing and the breast - it must read 165 F; colour or time alone never decides doneness.",
"cite": [
"S1",
"D1"
]
},
{
"text": "If the 16 lb bird is still frozen, it needs about 4 days thawing in the fridge, so it should already be there well ahead of the feast day.",
"cite": [
"S2",
"D1"
]
},
{
"text": "The carved turkey's 15-minute warm hold and the sides' short waits are all well inside the 2-hour safe window; still get any leftovers into the fridge within 2 hours of coming off the heat.",
"cite": [
"S3",
"D1"
]
}
],
"questions": [],
"caveats": [
"All times are estimates built on the page's steps and the USDA-based roast time - use the thermometer, not the clock, for the turkey.",
"You're free from about 10:50 to 14:45 while the turkey roasts hands-off - good time for table-setting or a break before the afternoon gets busy.",
"With one cook, jobs are kept one at a time; if anything slips, carving is the easiest job to hand off to a second pair of hands."
]
}
task: rescue - Classic, one oven (shift)
Input:
{
"task": "rescue",
"event": "{\"serve\":\"16:00\",\"earliest\":\"08:00\",\"date\":\"2026-11-26\",\"day_label\":\"Thu Nov 26\",\"guests\":10,\"allow_make_ahead\":true}",
"kitchen": "{\"ovens\":[{\"id\":\"oven1\",\"name\":\"Oven\",\"dishes_at_once\":2}],\"burners\":4,\"cooks\":1,\"extras\":[\"microwave\"]}",
"dishes": "[{\"id\":\"D1\",\"text\":\"Turkey, 16 lb\",\"name\":\"Whole turkey\",\"library\":\"turkey\",\"known\":true,\"hot\":true,\"weight_lb\":16,\"weight_assumed\":false,\"stuffed\":false,\"bought\":false,\"can_make_ahead_days\":0,\"wants_ahead\":false,\"steps\":[{\"label\":\"Season and truss\",\"res\":\"hands\",\"min\":25,\"active\":true},{\"label\":\"Roast\",\"res\":\"oven\",\"min\":255,\"temp_f\":325,\"full\":true},{\"label\":\"Rest under foil\",\"res\":\"counter\",\"min\":35},{\"label\":\"Carve\",\"res\":\"hands\",\"min\":15,\"active\":true}]},\"... 7 more dishes\"]",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Whole turkey (D1) is done at 165 F in the thickest part of the thigh, the wing and the breast - check with a thermometer, not the colour (USDA).\"},{\"id\":\"S2\",\"level\":\"caution\",\"text\":\"If the 16 lb turkey is frozen, thaw it in the fridge for about 4 days - roughly a day per 4 to 5 lb (USDA) - so move it to the fridge by Sun Nov 22.\"},{\"id\":\"S3\",\"level\":\"caution\",\"text\":\"Get leftovers into the fridge within 2 hours of coming off the heat (USDA 2-hour rule).\"}],\"notes\":[]}",
"note": "Just me cooking. My partner can carve if needed.",
"plan": "{\"serve\":\"16:00\",\"tasks\":[{\"id\":\"K1\",\"dish\":\"D8\",\"label\":\"Make the filling, fill the crust\",\"res\":\"hands\",\"start\":\"09:00\",\"end\":\"09:25\",\"day\":-1,\"active\":true},{\"id\":\"K2\",\"dish\":\"D8\",\"label\":\"Bake\",\"res\":\"oven1\",\"start\":\"09:25\",\"end\":\"10:20\",\"day\":-1,\"temp_f\":375},\"... 19 more steps\"]}",
"now": "14:40",
"problem": "{\"text\":\"The thermometer in the thickest part of the thigh reads 138 F, and the plan has the turkey coming out at 14:45. The sides are assembled and waiting.\",\"facts\":[{\"id\":\"P1\",\"text\":\"The thermometer in the thickest part of the thigh reads 138 F, and the plan has the turkey coming out at 14:45.\"},{\"id\":\"P2\",\"text\":\"The sides are assembled and waiting.\"}],\"safety\":[{\"id\":\"P3\",\"level\":\"caution\",\"text\":\"A poultry reading of 138 F is under 165 F - it is not done; keep cooking and check the thickest part of the thigh again (USDA).\"}],\"floor\":\"\",\"done\":[\"K1\",\"K2\",\"K3\",\"K4\",\"K5\",\"K6\",\"K7\",\"K8\",\"K10\"]}"
}
Reply:
{
"task": "rescue",
"verdict": "shift",
"title": "Turkey's behind - give it another hour, push dinner to about 5",
"summary": "At 14:40 the turkey's thigh reads only 138 F, well short of the 165 F it needs, so the plan's 14:45 pull time is off - it needs roughly another hour in the oven. The sides are already assembled and waiting, so nothing spoils by holding them a bit longer. Once the bird clears 165 F everywhere, rest it, then run the sides through the freed oven in the same order as planned, just about an hour later - serving moves from 16:00 to about 17:00.",
"serve": "17:00",
"changes": [
{
"id": "K9",
"start": "10:30",
"end": "14:40",
"text": "Closes out the roast task at now instead of the planned 14:45 - the thigh is only 138 F, so the bird isn't actually done; the rest of the roast continues as R1.",
"cite": [
"D1",
"P1",
"P3",
"N-now"
]
},
{
"id": "K11",
"start": "15:45",
"end": "16:30",
"text": "Stuffing bake waits for the oven, which doesn't free up until the turkey comes out at about 15:45 instead of 14:45.",
"cite": [
"D2",
"N-now"
]
},
{
"id": "K12",
"start": "15:45",
"end": "16:20",
"text": "Sweet potato bake slides the same hour, sharing the oven with the stuffing as planned.",
"cite": [
"D5",
"N-now"
]
},
"..."
],
"added": [
{
"id": "R1",
"dish": "D1",
"label": "Continue roasting to 165 F in the thigh",
"res": "oven1",
"start": "14:40",
"end": "15:45",
"temp_f": 325,
"full": true,
"active": false,
"cite": [
"D1",
"S1",
"P1",
"P3",
"N-now"
]
}
],
"dropped": [],
"steps": [
{
"text": "Leave the turkey in and keep roasting at 325 F - 138 F is well short of done, so it's not ready to come out.",
"cite": [
"N-now",
"P1",
"P3"
]
},
{
"text": "Recheck the thickest part of the thigh, plus the wing and breast, with the thermometer every 15-20 minutes; expect it to take until about 15:45 to clear 165 F everywhere.",
"cite": [
"D1",
"S1",
"P3"
]
},
{
"text": "Leave the assembled sides covered on the counter - a longer wait is no safety problem, well inside the 2-hour window.",
"cite": [
"P2"
]
},
"..."
],
"tell_guests": "Dinner's moving to about 5 instead of 4 - the turkey just needs a bit more time in the oven.",
"safety": [
{
"text": "138 F is not safe to serve - keep roasting until an instant-read thermometer reads at least 165 F in the thigh, wing and breast; colour alone never decides doneness.",
"cite": [
"P3",
"S1",
"D1"
]
},
{
"text": "The assembled sides waiting on the counter are fine for now; once anything comes off the heat, get it hot again or into the fridge within 2 hours.",
"cite": [
"P2",
"S3"
]
}
],
"caveats": [
"The new times are estimates - trust the thermometer, not the clock, for when the turkey is actually done.",
"If the thigh is still under 165 F by about 15:45, give it more time and let the rest of the schedule slide with it rather than pulling it early."
]
}
task: plan - Small Friendsgiving (tight)
Input:
{
"task": "plan",
"event": "{\"serve\":\"19:00\",\"earliest\":\"13:00\",\"date\":\"2026-11-21\",\"day_label\":\"Sat Nov 21\",\"guests\":6,\"allow_make_ahead\":false}",
"kitchen": "{\"ovens\":[{\"id\":\"oven1\",\"name\":\"Oven\",\"dishes_at_once\":2}],\"burners\":4,\"cooks\":2,\"extras\":[]}",
"dishes": "[{\"id\":\"D1\",\"text\":\"Roast chicken, 5 lb\",\"name\":\"Roast chicken\",\"library\":\"chicken\",\"known\":true,\"hot\":true,\"weight_lb\":5,\"weight_assumed\":false,\"stuffed\":false,\"bought\":false,\"can_make_ahead_days\":0,\"wants_ahead\":false,\"steps\":[{\"label\":\"Season\",\"res\":\"hands\",\"min\":10,\"active\":true},{\"label\":\"Roast\",\"res\":\"oven\",\"min\":90,\"temp_f\":350},{\"label\":\"Rest\",\"res\":\"counter\",\"min\":15},{\"label\":\"Carve\",\"res\":\"hands\",\"min\":10,\"active\":true}]},\"... 4 more dishes\"]",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Roast chicken (D1) is done at 165 F in the thickest part of the thigh, the wing and the breast - check with a thermometer, not the colour (USDA).\"},{\"id\":\"S2\",\"level\":\"caution\",\"text\":\"Get leftovers into the fridge within 2 hours of coming off the heat (USDA 2-hour rule).\"}],\"notes\":[],\"draft\":{\"tasks\":[{\"id\":\"K1\",\"dish\":\"D1\",\"label\":\"Season\",\"res\":\"hands\",\"start\":\"16:45\",\"end\":\"16:55\",\"day\":0,\"active\":true},{\"id\":\"K2\",\"dish\":\"D5\",\"label\":\"Assemble\",\"res\":\"hands\",\"start\":\"16:50\",\"end\":\"17:10\",\"day\":0,\"active\":true},\"... 9 more steps\"],\"clashes\":[],\"could_not_place\":[],\"moved_ahead\":[]}}",
"note": "Two of us cooking in a small apartment kitchen. Everything made on the day."
}
Reply:
{
"task": "plan",
"verdict": "tight",
"title": "Friendsgiving for 6: one oven, two cooks, 7 pm",
"summary": "The chicken claims the oven first at about 350 F for 90 minutes while the apple crisp shares the second slot; mac and cheese then takes that same slot at 375 F so it is out by 18:25, well ahead of serving. The vegetables get the oven alone at 425 F right as the chicken comes out, finishing hot at 18:55 while the chicken rests, gets carved, and the salad is dressed. Everything is ready by 19:00 with the oven and two cooks never over capacity, but the schedule runs close to the wire with little room for delay.",
"serve": "19:00",
"tasks": [
{
"id": "K1",
"dish": "D1",
"label": "Season",
"res": "hands",
"start": "16:45",
"end": "16:55",
"active": true,
"full": false,
"day": 0,
"cite": [
"D1",
"I-kitchen"
]
},
{
"id": "K2",
"dish": "D5",
"label": "Assemble",
"res": "hands",
"start": "16:50",
"end": "17:10",
"active": true,
"full": false,
"day": 0,
"cite": [
"D5",
"I-kitchen"
]
},
{
"id": "K3",
"dish": "D1",
"label": "Roast",
"res": "oven1",
"start": "16:55",
"end": "18:25",
"temp_f": 350,
"active": false,
"full": false,
"day": 0,
"cite": [
"D1",
"S1",
"E-event"
]
},
"..."
],
"moves": [
{
"text": "Chicken claims the oven first at 350 F; its roughly 25-minute rest starting at 18:25 is the kitchen's busiest window - the vegetables go in then at 425 F and the salad gets dressed, so nothing is asked to share a temperature with the chicken once it's out.",
"cite": [
"D1",
"D2",
"D4"
]
},
{
"text": "Mac and cheese bakes at 375 F alongside the chicken (25 F apart, within the oven's limit) so it finishes at 18:25; a dense cheese casserole holds heat well under foil, so sitting covered on the counter for about 35 minutes with no microwave or slow cooker in this kitchen is fine.",
"cite": [
"D1",
"D3",
"I-kitchen"
]
},
{
"text": "This is tight, not loose: about 10 minutes of slack is built into the chicken's rest (25 minutes scheduled against a 15-minute minimum), but the vegetables finish at 18:55 and carving ends right at 19:00 - if the roast runs even 10-15 minutes over, dinner slides late.",
"cite": [
"D1",
"D2",
"E-event"
]
}
],
"cut": [],
"safety": [
{
"text": "Check the chicken (D1) with an instant-read thermometer, not the clock - it must read 165 F in the thickest part of the thigh, the wing and the breast before resting and carving.",
"cite": [
"S1",
"D1"
]
},
{
"text": "Get anything left over into the fridge within 2 hours of coming off the heat.",
"cite": [
"S2"
]
}
],
"questions": [],
"caveats": [
"Times and temperatures are estimates - go by the thermometer, not the schedule.",
"Carryover heat should carry the chicken a further 5-10 F while it rests; that's a bonus on top of the 165 F check, not a substitute for it.",
"With no microwave or slow cooker, the mac and cheese and the carved chicken both just sit covered on the counter until serving - cover both loosely with foil as soon as they're out."
]
}
task: rescue - Small Friendsgiving (on_track)
Input:
{
"task": "rescue",
"event": "{\"serve\":\"19:00\",\"earliest\":\"13:00\",\"date\":\"2026-11-21\",\"day_label\":\"Sat Nov 21\",\"guests\":6,\"allow_make_ahead\":false}",
"kitchen": "{\"ovens\":[{\"id\":\"oven1\",\"name\":\"Oven\",\"dishes_at_once\":2}],\"burners\":4,\"cooks\":2,\"extras\":[]}",
"dishes": "[{\"id\":\"D1\",\"text\":\"Roast chicken, 5 lb\",\"name\":\"Roast chicken\",\"library\":\"chicken\",\"known\":true,\"hot\":true,\"weight_lb\":5,\"weight_assumed\":false,\"stuffed\":false,\"bought\":false,\"can_make_ahead_days\":0,\"wants_ahead\":false,\"steps\":[{\"label\":\"Season\",\"res\":\"hands\",\"min\":10,\"active\":true},{\"label\":\"Roast\",\"res\":\"oven\",\"min\":90,\"temp_f\":350},{\"label\":\"Rest\",\"res\":\"counter\",\"min\":15},{\"label\":\"Carve\",\"res\":\"hands\",\"min\":10,\"active\":true}]},\"... 4 more dishes\"]",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Roast chicken (D1) is done at 165 F in the thickest part of the thigh, the wing and the breast - check with a thermometer, not the colour (USDA).\"},{\"id\":\"S2\",\"level\":\"caution\",\"text\":\"Get leftovers into the fridge within 2 hours of coming off the heat (USDA 2-hour rule).\"}],\"notes\":[]}",
"note": "Two of us cooking in a small apartment kitchen. Everything made on the day.",
"plan": "{\"serve\":\"19:00\",\"tasks\":[{\"id\":\"K1\",\"dish\":\"D1\",\"label\":\"Season\",\"res\":\"hands\",\"start\":\"16:45\",\"end\":\"16:55\",\"day\":0,\"active\":true},{\"id\":\"K2\",\"dish\":\"D5\",\"label\":\"Assemble\",\"res\":\"hands\",\"start\":\"16:50\",\"end\":\"17:10\",\"day\":0,\"active\":true},\"... 9 more steps\"]}",
"now": "18:30",
"problem": "{\"text\":\"Everything has gone to the timeline so far: the chicken came out at 18:25 reading 167 F in the thigh and is resting, the mac and cheese is out, and the vegetables just went in. Are we still fine for 19:00?\",\"facts\":[{\"id\":\"P1\",\"text\":\"Everything has gone to the timeline so far: the chicken came out at 18:25 reading 167 F in the thigh and is resting, the mac and cheese is out, and the vegetables just went in.\"},{\"id\":\"P2\",\"text\":\"Are we still fine for 19:00?\"}],\"safety\":[{\"id\":\"P3\",\"level\":\"caution\",\"text\":\"Food sitting out (off the heat, out of the fridge) for about 0.1 hours - get it hot (165 F) or into the fridge before the 2-hour mark (USDA).\"}],\"floor\":\"\",\"done\":[\"K1\",\"K2\",\"K3\",\"K4\",\"K5\",\"K6\",\"K7\"]}"
}
Reply:
{
"task": "rescue",
"verdict": "on_track",
"title": "On track for 7 pm - just a slower oven swap",
"summary": "Everything matches the plan: the chicken cleared 165 F (167 F in the thigh) and came out right on schedule to rest, and the mac and cheese is out too. The oven needed a few minutes to climb from the mac's 375 F up to the vegetables' 425 F, so they went in closer to 18:30 than the draft's 18:25 - let them run to about 19:00 instead of 18:55. Resting, carving and the salad all still land the same, so dinner is still fine for 19:00.",
"serve": "19:00",
"changes": [
{
"id": "K9",
"start": "18:30",
"end": "19:00",
"res": "oven1",
"text": "The oven had to climb from 375 F (mac and cheese) to 425 F before the vegetables could go in, so they started a few minutes after the draft's 18:25 - give them until about 19:00 to finish instead of 18:55.",
"cite": [
"P1",
"K9",
"D2"
]
}
],
"added": [],
"dropped": [],
"steps": [
{
"text": "Chicken is safely past 165 F (167 F in the thigh) - no need to check it again before carving.",
"cite": [
"S1",
"P1"
]
},
{
"text": "Keep the chicken resting on the counter until about 18:50; carryover will carry it a little higher.",
"cite": [
"K8",
"D1"
]
},
{
"text": "Leave the vegetables roasting at 425 F until about 19:00 - a few minutes longer than the original 18:55.",
"cite": [
"K9",
"D2"
]
},
"..."
],
"tell_guests": "",
"safety": [
{
"text": "The chicken cleared the 165 F safety mark (167 F in the thigh) before coming off the heat.",
"cite": [
"S1",
"P1"
]
},
{
"text": "The chicken and the mac and cheese have only been out of the oven a few minutes - well inside the 2-hour window; still get everything served or into the fridge within that time.",
"cite": [
"P3",
"S2"
]
}
],
"caveats": [
"If the vegetables aren't quite browned at 19:00, give them another minute or two - the resting chicken and the covered mac and cheese can both hold a little longer without any food-safety concern."
]
}
task: plan - Big bird, late start (overbooked)
Input:
{
"task": "plan",
"event": "{\"serve\":\"14:00\",\"earliest\":\"10:00\",\"date\":\"2026-11-26\",\"day_label\":\"Thu Nov 26\",\"guests\":16,\"allow_make_ahead\":false}",
"kitchen": "{\"ovens\":[{\"id\":\"oven1\",\"name\":\"Oven\",\"dishes_at_once\":2}],\"burners\":3,\"cooks\":1,\"extras\":[]}",
"dishes": "[{\"id\":\"D1\",\"text\":\"Turkey, 22 lb, stuffed\",\"name\":\"Whole turkey\",\"library\":\"turkey\",\"known\":true,\"hot\":true,\"weight_lb\":22,\"weight_assumed\":false,\"stuffed\":true,\"bought\":false,\"can_make_ahead_days\":0,\"wants_ahead\":false,\"steps\":[{\"label\":\"Season and truss\",\"res\":\"hands\",\"min\":25,\"active\":true},{\"label\":\"Roast\",\"res\":\"oven\",\"min\":315,\"temp_f\":325,\"full\":true},{\"label\":\"Rest under foil\",\"res\":\"counter\",\"min\":35},{\"label\":\"Carve\",\"res\":\"hands\",\"min\":15,\"active\":true}]},\"... 7 more dishes\"]",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Whole turkey (D1) is done at 165 F in the thickest part of the thigh, the wing and the breast - check with a thermometer, not the colour (USDA).\"},{\"id\":\"S2\",\"level\":\"caution\",\"text\":\"Stuffing cooked inside the bird must reach 165 F in its centre too, which slows the roast (USDA). Baking it in a dish is easier to time.\"},{\"id\":\"S3\",\"level\":\"caution\",\"text\":\"If the 22 lb turkey is frozen, thaw it in the fridge for about 5 days - roughly a day per 4 to 5 lb (USDA) - so move it to the fridge by Sat Nov 21.\"},{\"id\":\"S4\",\"level\":\"caution\",\"text\":\"Get leftovers into the fridge within 2 hours of coming off the heat (USDA 2-hour rule).\"}],\"notes\":[],\"draft\":{\"tasks\":[{\"id\":\"K1\",\"dish\":\"D1\",\"label\":\"Season and truss\",\"res\":\"hands\",\"start\":\"07:30\",\"end\":\"07:55\",\"day\":0,\"active\":true},{\"id\":\"K2\",\"dish\":\"D1\",\"label\":\"Roast\",\"res\":\"oven1\",\"start\":\"07:55\",\"end\":\"13:10\",\"day\":0,\"temp_f\":325,\"full\":true},\"... 17 more steps\"],\"clashes\":[{\"id\":\"X1\",\"level\":\"clash\",\"code\":\"early\",\"text\":\"K1 starts at 07:30, before the earliest start of 10:00.\"},{\"id\":\"X2\",\"level\":\"clash\",\"code\":\"early\",\"text\":\"K2 starts at 07:55, before the earliest start of 10:00.\"},{\"id\":\"X3\",\"level\":\"clash\",\"code\":\"oven_full\",\"text\":\"11:05-12:00: more in the oven than fits (2 dishes at once; a big roast fills it) - K2, K5, K6.\"},{\"id\":\"X4\",\"level\":\"clash\",\"code\":\"oven_full\",\"text\":\"13:25-13:40: more in the oven than fits (2 dishes at once; a big roast fills it) - K14, K16, K17.\"},{\"id\":\"X5\",\"level\":\"clash\",\"code\":\"oven_full\",\"text\":\"13:40-13:55: more in the oven than fits (2 dishes at once; a big roast fills it) - K14, K16, K17, K18.\"},{\"id\":\"X6\",\"level\":\"clash\",\"code\":\"hands\",\"text\":\"13:10-13:20: 2 hands-on jobs at once with 1 cook - K11, K12.\"},{\"id\":\"X7\",\"level\":\"clash\",\"code\":\"temp_clash\",\"text\":\"K2 wants 325 F and K5 wants 375 F in the same oven from 11:05.\"},{\"id\":\"X8\",\"level\":\"clash\",\"code\":\"temp_clash\",\"text\":\"K2 wants 325 F and K6 wants 375 F in the same oven from 11:05.\"}],\"could_not_place\":[\"D1\",\"D2\",\"D5\"],\"moved_ahead\":[]}}",
"note": "Everything fresh on the day, nothing made ahead. Lunch at 2 sharp - the grandparents leave at 4."
}
Reply:
{
"task": "plan",
"verdict": "overbooked",
"title": "16 for lunch at 2: the turkey alone rules it out",
"summary": "A 22 lb stuffed turkey needs about 25 minutes to truss, 5 hours 15 minutes to roast, 35 minutes to rest and 15 minutes to carve - starting at the 10:00 earliest, that is roughly 6.5 hours, landing the carved bird around 16:30, not 14:00, before a single side dish competes for the one oven it fills completely while it roasts. This plan keeps the turkey on the earliest possible start, uses its 35-minute rest as the oven's busiest window for sides, and pairs dishes two at a time by matching oven temperature, so everything does get cooked safely - but dinner lands hours after 14:00 and after the grandparents' 16:00 departure. Something has to give: start well before 10:00, accept a much later serve, or buy a ready-made turkey.",
"serve": "14:00",
"tasks": [
{
"id": "K1",
"dish": "D1",
"label": "Season and truss",
"res": "hands",
"start": "10:00",
"end": "10:25",
"full": false,
"active": true,
"day": 0,
"cite": [
"D1",
"E-event"
]
},
{
"id": "K2",
"dish": "D1",
"label": "Roast",
"res": "oven1",
"start": "10:25",
"end": "15:40",
"temp_f": 325,
"full": true,
"active": false,
"day": 0,
"cite": [
"D1",
"S1",
"S2",
"I-kitchen"
]
},
{
"id": "K3",
"dish": "D6",
"label": "Make the filling, fill the crust",
"res": "hands",
"start": "10:25",
"end": "10:50",
"full": false,
"active": true,
"day": 0,
"cite": [
"D6"
]
},
"..."
],
"moves": [
{
"text": "The free draft started trussing at 07:30 and the roast at 07:55, both before the 10:00 earliest start - this plan never starts before 10:00, which is exactly why the turkey cannot be resting, let alone carved, by 14:00.",
"cite": [
"X1",
"X2",
"D1",
"E-event"
]
},
{
"text": "The draft tried to bake both pies at 375 F in the same oven the 325 F turkey was roasting in - impossible, since the turkey's roast is a 'full' task that occupies the whole oven for its entire 5 hour 15 minute run. Both pies now bake only after the turkey comes out, back to back with the other 375 F dishes.",
"cite": [
"X7",
"X8",
"D1",
"D6",
"D7"
]
},
{
"text": "The draft also tried to fit the mac and cheese, green beans and rolls into the oven on top of the turkey (X4, X5) - fixed by moving all of them to the turkey's 35-minute rest window, paired two at a time by matching oven temperature (rolls with mac and cheese at 375 F, then sweet potato with green beans at 350 F), which is the oven's busiest and most useful stretch once the big roast comes out.",
"cite": [
"X4",
"X5",
"D2",
"D3",
"D4",
"D5",
"D1"
]
},
"..."
],
"cut": [
{
"text": "Start the turkey about 150 minutes earlier than the stated earliest (trussing by about 07:30 instead of 10:00) - the only way this bird is resting and carved anywhere near 14:00.",
"cite": [
"D1",
"E-event"
]
},
{
"text": "Or accept serving around 16:30 instead of 14:00 (about 150 minutes later) - what a 10:00 earliest start and a 22 lb stuffed turkey actually allow in one oven; note this still misses the stated 16:00 departure by about 30 minutes.",
"cite": [
"D1",
"E-event"
]
},
{
"text": "Buy a fully-cooked, ready-to-reheat turkey instead of roasting a raw one - this removes essentially all of the roughly 5-hour-15-minute roast plus its 35-minute rest, the only change that keeps both the 10:00 earliest start and the 14:00 serve.",
"cite": [
"D1",
"E-event"
]
},
"..."
],
"safety": [
{
"text": "Check the turkey, including the stuffing inside it, for 165 F in the thickest part of the thigh, the wing and the breast with a thermometer before carving - color is not a reliable guide.",
"cite": [
"S1",
"S2",
"D1"
]
},
{
"text": "The stuffing must also clear 165 F in its center, which is part of why a stuffed bird this size roasts toward the longest end of the timetable.",
"cite": [
"S2",
"D1"
]
},
{
"text": "Carryover heat will carry the turkey another 5-10 F while it rests under foil - a safety margin on top of the 165 F check, not a reason to pull it early.",
"cite": [
"D1",
"S1"
]
},
"..."
],
"questions": [],
"caveats": [
"This is the best partial timeline physics allows, not a working lunch plan: everything after about 11:15 lands well past the stated 14:00 serve and past the 16:00 departure the host mentioned.",
"With one oven, no microwave and no slow cooker, nothing here can hold a finished side hot for long - plan to serve each one as it comes out rather than batching everything for a single moment.",
"Gravy is scheduled after carving because it needs the pan drippings; it will be the very last thing ready."
]
}
task: rescue - Big bird, late start (cut)
Input:
{
"task": "rescue",
"event": "{\"serve\":\"14:00\",\"earliest\":\"10:00\",\"date\":\"2026-11-26\",\"day_label\":\"Thu Nov 26\",\"guests\":16,\"allow_make_ahead\":false}",
"kitchen": "{\"ovens\":[{\"id\":\"oven1\",\"name\":\"Oven\",\"dishes_at_once\":2}],\"burners\":3,\"cooks\":1,\"extras\":[]}",
"dishes": "[{\"id\":\"D1\",\"text\":\"Turkey, 22 lb, stuffed\",\"name\":\"Whole turkey\",\"library\":\"turkey\",\"known\":true,\"hot\":true,\"weight_lb\":22,\"weight_assumed\":false,\"stuffed\":true,\"bought\":false,\"can_make_ahead_days\":0,\"wants_ahead\":false,\"steps\":[{\"label\":\"Season and truss\",\"res\":\"hands\",\"min\":25,\"active\":true},{\"label\":\"Roast\",\"res\":\"oven\",\"min\":315,\"temp_f\":325,\"full\":true},{\"label\":\"Rest under foil\",\"res\":\"counter\",\"min\":35},{\"label\":\"Carve\",\"res\":\"hands\",\"min\":15,\"active\":true}]},\"... 7 more dishes\"]",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Whole turkey (D1) is done at 165 F in the thickest part of the thigh, the wing and the breast - check with a thermometer, not the colour (USDA).\"},{\"id\":\"S2\",\"level\":\"caution\",\"text\":\"Stuffing cooked inside the bird must reach 165 F in its centre too, which slows the roast (USDA). Baking it in a dish is easier to time.\"},{\"id\":\"S3\",\"level\":\"caution\",\"text\":\"If the 22 lb turkey is frozen, thaw it in the fridge for about 5 days - roughly a day per 4 to 5 lb (USDA) - so move it to the fridge by Sat Nov 21.\"},{\"id\":\"S4\",\"level\":\"caution\",\"text\":\"Get leftovers into the fridge within 2 hours of coming off the heat (USDA 2-hour rule).\"}],\"notes\":[]}",
"note": "Everything fresh on the day, nothing made ahead. Lunch at 2 sharp - the grandparents leave at 4.",
"plan": "{\"serve\":\"14:00\",\"tasks\":[{\"id\":\"K1\",\"dish\":\"D1\",\"label\":\"Season and truss\",\"res\":\"hands\",\"start\":\"10:00\",\"end\":\"10:25\",\"day\":0,\"active\":true},{\"id\":\"K2\",\"dish\":\"D1\",\"label\":\"Roast\",\"res\":\"oven1\",\"start\":\"10:25\",\"end\":\"15:40\",\"day\":0,\"temp_f\":325,\"full\":true},\"... 17 more steps\"]}",
"now": "12:30",
"problem": "{\"text\":\"The oven is full and the turkey still has hours to go. Guests cannot wait past 14:30. What do we drop or change so we eat by 14:30?\",\"facts\":[{\"id\":\"P1\",\"text\":\"The oven is full and the turkey still has hours to go.\"},{\"id\":\"P2\",\"text\":\"Guests cannot wait past 14:30.\"},{\"id\":\"P3\",\"text\":\"What do we drop or change so we eat by 14:30?\"}],\"safety\":[],\"floor\":\"\",\"done\":[\"K1\",\"K3\",\"K4\"]}"
}
Reply:
{
"task": "rescue",
"verdict": "cut",
"title": "Turkey rules the clock: dinner lands near 4:30, not 2:30",
"summary": "The 22 lb stuffed turkey went in at 10:25 and needs about 5 hours 15 minutes of roasting no matter what - at 12:30 it is only 2 hours in, so it cannot come out before about 15:40, and resting and carving push the bird itself to about 16:30. There is no safe way to have it on the table by 14:30; the honest fix is to take all three sides off the oven (it is full with the turkey until mid-afternoon) and make them on the stovetop instead, move the gravy to the moment the turkey comes out rather than after carving, and push the pies to bake and cool after the meal as a separate dessert.",
"serve": "16:30",
"changes": [
{
"id": "K6",
"start": "12:30",
"end": "12:55",
"text": "Boil and mash the sweet potatoes now, while the cook is free and the burners are idle - no reason to wait for the original 15:10 slot.",
"cite": [
"D3",
"N-now",
"P1"
]
},
{
"id": "K7",
"start": "12:55",
"end": "13:05",
"text": "Skip assembling it into a baking dish - mash in the butter and season it, then hold it. No oven slot opens up before mid-afternoon, so plain mashed replaces the casserole bake.",
"cite": [
"D3",
"I-kitchen",
"P1"
]
},
{
"id": "K11",
"start": "13:05",
"end": "13:20",
"text": "Combine the green beans, cream soup and fried onions in a saucepan instead of a baking dish - same mixture, stovetop finish.",
"cite": [
"D2",
"I-kitchen",
"P1"
]
},
"..."
],
"added": [],
"dropped": [
{
"dish": "D6",
"text": "Pecan pie cannot bake until the oven is free again after the rolls, around 15:58, and then needs about 55 minutes baking plus cooling - it will not be ready for this serving. Bake it after the meal and serve it as a later dessert.",
"cite": [
"D6",
"I-kitchen",
"P1"
]
},
{
"dish": "D7",
"text": "Same problem as the pecan pie - the oven has no opening before the rolls finish, so pumpkin pie also bakes after the meal instead of before it.",
"cite": [
"D7",
"I-kitchen",
"P1"
]
}
],
"steps": [
{
"text": "Leave the turkey alone - at 12:30 it is about 2 hours into a roughly 5 hour 15 minute roast, so it has about 3 more hours no matter what is done elsewhere.",
"cite": [
"N-now",
"D1",
"P1"
]
},
{
"text": "Boil and mash the sweet potatoes, then combine and warm the green beans, then boil the pasta and sauce the mac and cheese - back to back on the stovetop, skipping all three bakes.",
"cite": [
"D3",
"D2",
"D4",
"I-kitchen"
]
},
{
"text": "Cover all three and hold them on the counter; the oven will not be free again until the turkey comes out.",
"cite": [
"I-kitchen"
]
},
"..."
],
"tell_guests": "Dinner lands closer to 4:30 than 2 - the turkey needs the time - but the sides will be ready to pick at well before then if anyone gets hungry.",
"safety": [
{
"text": "Check the turkey, and the stuffing inside it, for 165 F in the thickest part of the thigh, wing and breast with a thermometer before carving - never serve it early just to hit a clock target.",
"cite": [
"S1",
"S2",
"D1",
"P1",
"P2"
]
},
{
"text": "Get anything not eaten within 2 hours of coming off the heat into the fridge.",
"cite": [
"S4"
]
}
],
"caveats": [
"There is no safe way to shorten a stuffed 22 lb turkey's roast, and one oven cannot bake sides alongside it - 16:30 is the honest earliest the full, safe dinner comes together, not 14:30.",
"The note mentions the grandparents leave at 16:00; even this rescued timeline carves the turkey at about 16:15, after they would have left. That is a separate, harder problem than the 14:30 ask and this plan cannot close it without cutting the turkey itself.",
"Mac and cheese and the green beans will sit covered for over an hour before serving - expect to stir and loosen them before plating."
]
}
task: plan - Dinner for 12? (need_info)
Input:
{
"task": "plan",
"event": "{\"serve\":null,\"earliest\":null,\"date\":null,\"day_label\":null,\"guests\":12,\"allow_make_ahead\":true}",
"kitchen": "{\"ovens\":[{\"id\":\"oven1\",\"name\":\"Oven\",\"dishes_at_once\":2}],\"burners\":4,\"cooks\":1,\"extras\":[]}",
"dishes": "[{\"id\":\"D1\",\"text\":\"food for 12\",\"name\":\"Other dish\",\"library\":\"other\",\"known\":false,\"hot\":true,\"weight_lb\":null,\"weight_assumed\":false,\"stuffed\":false,\"bought\":false,\"can_make_ahead_days\":0,\"wants_ahead\":false,\"steps\":[{\"label\":\"Prep\",\"res\":\"hands\",\"min\":15,\"active\":true},{\"label\":\"Cook\",\"res\":\"burner\",\"min\":20}]}]",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Get leftovers into the fridge within 2 hours of coming off the heat (USDA 2-hour rule).\"}],\"notes\":[\"D1 (food for 12) is not in the step library - the draft uses a 15-minute prep and 20 minutes on a burner as a placeholder.\"],\"draft\":{\"tasks\":[],\"clashes\":[],\"could_not_place\":[],\"moved_ahead\":[]}}",
"note": "Hosting for the first time."
}
Reply:
{
"task": "plan",
"verdict": "need_info",
"title": "Need a serve time and a real menu",
"summary": "There is no serve time or earliest start on this feast day, and D1 is only logged as \"food for 12\" with placeholder prep and burner steps - not an actual dish. A relay timeline has to be built backwards from a serve time against real dishes, so I cannot place anything in the oven yet.",
"serve": null,
"tasks": [],
"moves": [],
"cut": [],
"safety": [
{
"text": "Whatever ends up on the menu, get any leftovers into the fridge within 2 hours of coming off the heat.",
"cite": [
"S1"
]
}
],
"questions": [
"What time do you want to serve dinner, and how early could you realistically start cooking that day?",
"What dishes are actually on the menu (e.g. turkey, stuffing, mashed potatoes, a pie) instead of one line for \"food for 12\"?",
"Is there a main roast, and if so what kind and about how many pounds?",
"..."
],
"caveats": []
}
task: plan - Cooked early, left out (fits)
Input:
{
"task": "plan",
"event": "{\"serve\":\"17:00\",\"earliest\":\"07:00\",\"date\":\"2026-11-26\",\"day_label\":\"Thu Nov 26\",\"guests\":8,\"allow_make_ahead\":true}",
"kitchen": "{\"ovens\":[{\"id\":\"oven1\",\"name\":\"Oven\",\"dishes_at_once\":2}],\"burners\":4,\"cooks\":2,\"extras\":[\"microwave\",\"slow_cooker\"]}",
"dishes": "[{\"id\":\"D1\",\"text\":\"Turkey breast, 7 lb\",\"name\":\"Turkey breast\",\"library\":\"turkey_breast\",\"known\":true,\"hot\":true,\"weight_lb\":7,\"weight_assumed\":false,\"stuffed\":false,\"bought\":false,\"can_make_ahead_days\":0,\"wants_ahead\":false,\"steps\":[{\"label\":\"Season\",\"res\":\"hands\",\"min\":10,\"active\":true},{\"label\":\"Roast\",\"res\":\"oven\",\"min\":195,\"temp_f\":325},{\"label\":\"Rest\",\"res\":\"counter\",\"min\":20},{\"label\":\"Slice\",\"res\":\"hands\",\"min\":10,\"active\":true}]},\"... 5 more dishes\"]",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Turkey breast (D1) is done at 165 F in the thickest part of the thigh, the wing and the breast - check with a thermometer, not the colour (USDA).\"},{\"id\":\"S2\",\"level\":\"caution\",\"text\":\"Get leftovers into the fridge within 2 hours of coming off the heat (USDA 2-hour rule).\"}],\"notes\":[],\"draft\":{\"tasks\":[{\"id\":\"K1\",\"dish\":\"D6\",\"label\":\"Make the filling, fill the crust\",\"res\":\"hands\",\"start\":\"11:55\",\"end\":\"12:20\",\"day\":0,\"active\":true},{\"id\":\"K2\",\"dish\":\"D6\",\"label\":\"Bake\",\"res\":\"oven1\",\"start\":\"12:20\",\"end\":\"13:15\",\"day\":0,\"temp_f\":375},\"... 13 more steps\"],\"clashes\":[],\"could_not_place\":[],\"moved_ahead\":[]}}",
"note": "We like to get the cooking done early."
}
Reply:
{
"task": "plan",
"verdict": "fits",
"title": "Thanksgiving for 8 - one oven, turkey-breast relay",
"summary": "The pie and cranberry sauce move to the evening before, along with peeling the potatoes and mixing the stuffing, so feast-day itself starts with just the turkey. The stuffing shares the oven with the turkey for its last five minutes and then has it to itself through the turkey's rest, and the mashed potatoes move into the slow cooker to hold warm. Everything lands between about 16:30 and 16:55 for a 17:00 serve, with the oven, the burners and both cooks never double-booked.",
"serve": "17:00",
"tasks": [
{
"id": "K1",
"dish": "D4",
"label": "Simmer",
"res": "burner",
"start": "16:00",
"end": "16:15",
"active": true,
"day": -1,
"cite": [
"D4"
]
},
{
"id": "K2",
"dish": "D6",
"label": "Make the filling, fill the crust",
"res": "hands",
"start": "16:00",
"end": "16:25",
"active": true,
"day": -1,
"cite": [
"D6"
]
},
{
"id": "K3",
"dish": "D4",
"label": "Chill",
"res": "fridge",
"start": "16:15",
"end": "18:15",
"active": false,
"day": -1,
"cite": [
"D4"
]
},
"..."
],
"moves": [
{
"text": "Moved the pie and the cranberry sauce entirely to the evening before - both keep well, and you said you like to get cooking done early.",
"cite": [
"D6",
"D4"
]
},
{
"text": "Moved the potatoes' peeling and cutting and the stuffing's chop-saute-mix to the evening before too; keep the cut potatoes covered in cold water in the fridge and the stuffing mix covered, so only actual cooking is left for the day.",
"cite": [
"D3",
"D2"
]
},
{
"text": "Stretched the turkey's rest from 20 to about 35 minutes and used it as the oven's second shift: the stuffing goes in for the turkey's last 5 minutes in the oven (325 F and 350 F are close enough to share), then has the oven to itself once the turkey comes out to rest - no second oven needed.",
"cite": [
"D1",
"D2",
"I-kitchen"
]
},
"..."
],
"cut": [],
"safety": [
{
"text": "Judge the turkey breast only with an instant-read thermometer in the thickest part of the thigh, wing and breast - it is safe at about 165 F, regardless of the clock.",
"cite": [
"S1",
"D1"
]
},
{
"text": "Get anything hot into the fridge within about 2 hours of coming off the heat, and don't let the made-ahead cranberry sauce or pie sit out longer than that either once they're out of the fridge.",
"cite": [
"S2",
"D4",
"D6"
]
}
],
"questions": [],
"caveats": [
"All times are about - the turkey's 195-minute roast is the page's own estimate from its weight, so start checking its temperature a bit before the clock says it should be done.",
"The turkey breast will likely climb another 5-10 F while it rests - that's normal and keeps it juicy, not something to cook for.",
"The cranberry sauce can go straight from the fridge to the table, or sit out briefly to take the chill off.",
"..."
]
}
task: rescue - Cooked early, left out (food_safety_stop)
Input:
{
"task": "rescue",
"event": "{\"serve\":\"17:00\",\"earliest\":\"07:00\",\"date\":\"2026-11-26\",\"day_label\":\"Thu Nov 26\",\"guests\":8,\"allow_make_ahead\":true}",
"kitchen": "{\"ovens\":[{\"id\":\"oven1\",\"name\":\"Oven\",\"dishes_at_once\":2}],\"burners\":4,\"cooks\":2,\"extras\":[\"microwave\",\"slow_cooker\"]}",
"dishes": "[{\"id\":\"D1\",\"text\":\"Turkey breast, 7 lb\",\"name\":\"Turkey breast\",\"library\":\"turkey_breast\",\"known\":true,\"hot\":true,\"weight_lb\":7,\"weight_assumed\":false,\"stuffed\":false,\"bought\":false,\"can_make_ahead_days\":0,\"wants_ahead\":false,\"steps\":[{\"label\":\"Season\",\"res\":\"hands\",\"min\":10,\"active\":true},{\"label\":\"Roast\",\"res\":\"oven\",\"min\":195,\"temp_f\":325},{\"label\":\"Rest\",\"res\":\"counter\",\"min\":20},{\"label\":\"Slice\",\"res\":\"hands\",\"min\":10,\"active\":true}]},\"... 5 more dishes\"]",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Turkey breast (D1) is done at 165 F in the thickest part of the thigh, the wing and the breast - check with a thermometer, not the colour (USDA).\"},{\"id\":\"S2\",\"level\":\"caution\",\"text\":\"Get leftovers into the fridge within 2 hours of coming off the heat (USDA 2-hour rule).\"}],\"notes\":[]}",
"note": "We like to get the cooking done early.",
"plan": "{\"serve\":\"17:00\",\"tasks\":[{\"id\":\"K1\",\"dish\":\"D4\",\"label\":\"Simmer\",\"res\":\"burner\",\"start\":\"16:00\",\"end\":\"16:15\",\"day\":-1,\"active\":true},{\"id\":\"K2\",\"dish\":\"D6\",\"label\":\"Make the filling, fill the crust\",\"res\":\"hands\",\"start\":\"16:00\",\"end\":\"16:25\",\"day\":-1,\"active\":true},\"... 14 more steps\"]}",
"now": "16:20",
"problem": "{\"text\":\"We roasted the turkey breast and baked the stuffing this morning. Both have been sitting on the counter under foil since 11 am. Can we just reheat them in the oven before guests come at 17:00?\",\"facts\":[{\"id\":\"P1\",\"text\":\"We roasted the turkey breast and baked the stuffing this morning.\"},{\"id\":\"P2\",\"text\":\"Both have been sitting on the counter under foil since 11 am.\"},{\"id\":\"P3\",\"text\":\"Can we just reheat them in the oven before guests come at 17:00?\"}],\"safety\":[{\"id\":\"P4\",\"level\":\"stop\",\"text\":\"Cooked or perishable food has been sitting out (off the heat, out of the fridge) for about 5.3 hours - past the USDA 2-hour rule. Reheating does not make it safe; it should be thrown away.\"}],\"floor\":\"food_safety_stop\",\"done\":[\"K1\",\"K2\",\"K3\",\"K4\",\"K5\",\"K6\",\"K7\",\"K8\",\"K9\"]}"
}
Reply:
{
"task": "rescue",
"verdict": "food_safety_stop",
"title": "Toss the turkey and stuffing — dinner still lands at 5",
"summary": "The turkey breast and stuffing have been sitting out under foil since about 11 am — about 5.3 hours, well past the USDA 2-hour rule — so both, and the pan drippings that sat with them, must be thrown away rather than reheated. Dinner can still serve on time at 17:00 with mashed potatoes, a stock-only gravy, cranberry sauce and pie, which are all safe and on schedule.",
"serve": "17:00",
"changes": [
{
"id": "K13",
"start": "16:20",
"end": "16:40",
"text": "Make the gravy with stock only — skip the pan drippings, which sat out with the turkey since 11 am and aren't safe either.",
"cite": [
"P1",
"P2",
"P4",
"D5"
]
}
],
"added": [
{
"id": "R1",
"dish": "D1",
"label": "Discard the turkey, the stuffing and the pan drippings (unsafe, out since about 11 am)",
"res": "counter",
"start": "16:20",
"end": "16:22",
"active": false,
"cite": [
"D1",
"D2",
"P1",
"P2",
"P4"
]
}
],
"dropped": [
{
"dish": "D1",
"text": "Turkey breast has been out since about 11 am, about 5.3 hours — past the 2-hour rule. Throw it away; do not reheat it.",
"cite": [
"P1",
"P2",
"P4"
]
},
{
"dish": "D2",
"text": "Stuffing was baked and has been sitting with the turkey since 11 am, the same unsafe stretch. Throw it away; do not reheat it.",
"cite": [
"P1",
"P2",
"P4"
]
}
],
"steps": [
{
"text": "Throw away the turkey breast, the stuffing, and the pan drippings right now — they've been out since about 11 am, past the 2-hour safety window.",
"cite": [
"P1",
"P2",
"P4",
"N-now"
]
},
{
"text": "Leave the potatoes boiling as planned; mash with butter around 16:25 and hold warm in the slow cooker until serving.",
"cite": [
"D3",
"N-now"
]
},
{
"text": "Make the gravy on the burner now using stock only, no drippings.",
"cite": [
"D5",
"P4",
"N-now"
]
},
"..."
],
"tell_guests": "Dinner's still at 5 — we're skipping the turkey and stuffing tonight for food-safety reasons, but potatoes, gravy, cranberry sauce and pie are all set.",
"safety": [
{
"text": "The turkey and the stuffing have been out of the fridge since about 11 am, about 5.3 hours — past the 2-hour USDA limit. They must be thrown away; reheating will not make them safe.",
"cite": [
"P1",
"P2",
"P4"
]
},
{
"text": "The pan drippings sat out with the turkey for the same stretch, so they are not safe for gravy either — use stock only.",
"cite": [
"P1",
"P4",
"D5"
]
}
],
"caveats": [
"This treats the stuffing as unsafe along with the turkey, matching what you described, even though the page's task list had not yet marked the stuffing bake as finished — if it truly was not baked yet, there is nothing to discard, just drop it from tonight's menu.",
"Gravy made from stock alone will be thinner and less rich than the pan-drippings version.",
"This leaves dinner without a hot protein main; nothing else in tonight's menu can be swapped in within the time left."
]
}
Build the input yourself
Load the script in node (global.window = {}; require("./relaykit.js")), then:
const K = RelayKit;
const pre = K.prescan({ date: "2026-11-26", serve: "16:00", earliest: "08:00", guests: 10, cooks: 1, slots: 2, burners: 4,
extras: ["microwave"], ahead: true, menu: "Turkey, 16 lb\nStuffing\nMashed potatoes\nRolls\nPumpkin pie" });
const draft = K.draft(pre), clashes = K.check(draft.tasks, pre); // the free draft and its clashes
const wire = K.toWire(K.buildInput("plan", { pre, draft, note: "" }).input); // POST this to /estimate and /run
// later: const plan = K.normalize(K.parseReply(job.output.output));
// K.reconcile(plan, { pre, draftX: clashes }) -> [{level, code, text}]
// on the day: K.buildInput("rescue", { pre, draft, plan, now: "15:05", problem: "The thigh reads 138 F", done: [] })
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://oven-relay.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://oven-relay.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"]
// oven-relay.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://oven-relay.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://oven-relay.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
}
// OvenRelay.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 OvenRelay {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://oven-relay.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://oven-relay.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://oven-relay.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://oven-relay.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":"oven-relay"}' | 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": "oven-relay"})["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: "oven-relay" })).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": "oven-relay"}, 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", "oven-relay"), 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: "oven-relay" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "oven-relay"])["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 = "oven-relay" }))!["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": "{...}", "kitchen": "{...}", "dishes": "[...]", "checks": "{...}"}
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": "{...}", "kitchen": "{...}", "dishes": "[...]", "checks": "{...}"}
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, event, kitchen, dishes, checks, note} (rescue: + plan, now, problem)
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 oven-relay:<task>:<inputHash>:a<attempt>, and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="oven-relay: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}'
import hashlib, time
key = "oven-relay:" + 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 lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `oven-relay:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "oven-relay:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "oven-relay:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "oven-relay:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
$key = "oven-relay:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"oven-relay:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: oven-relay:plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
feast = json.load(open("input.json")) # the same run input as input.json: {"task": "plan", "event": "{...}", "kitchen": "{...}", "dishes": "[...]", "checks": "{...}"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(feast).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "oven-relay:plan:" + hashlib.sha256(json.dumps(feast).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 feast = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(feast), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `oven-relay:plan:${createHash("sha256").update(JSON.stringify(feast)).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", "oven-relay: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 feast = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(feast.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "oven-relay:plan:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(feast)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
feast = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "oven-relay:plan:#{Digest::SHA256.hexdigest(feast)[0, 16]}:a1")
req.body = feast
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
$feast = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $feast, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: oven-relay:plan:" . substr(hash("sha256", $feast), 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 feast = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(feast));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(feast, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"oven-relay: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);