Skip to main content

Optimize Loop API (Phase 5 E6)

All endpoints verified working via curl against a real dev server (real Postgres, a real BullMQ worker process — apps/worker — consuming a real local Redis, and a real OpenAI account behind the gateway connection — no mocked fetch, no mocked queue). Document updated only after curl confirmation, per CLAUDE.md's API-reference rule.

The optimize loop closes the feedback → optimize → promote cycle: given a prompt's production version and a dataset of failing/target cases, an LLM ("the optimizer") drafts up to draft_count candidate rewrites of the template, then those candidates (plus the production baseline) are run through the exact same E3/E4/E5 machinery — grid resolution, BullMQ cell execution via GatewayService.complete, and judge scoring — so a candidate's score is directly comparable to production's. A human then inspects GET /runs/:id/report (E5, documented in experiments.md — not duplicated here) and, if a candidate is a genuine improvement, promotes it with POST /runs/:id/promote. Promotion is the only path that turns a disposable PromptCandidate into a real, immutable PromptVersion — nothing is ever auto-promoted.

Auth: requireAnyAuth (session cookie or personal/team API key) on POST /prompts/:promptId/optimize. POST /runs/:id/promote additionally requires requireRole('owner', 'admin', 'editor') — the same guard used by the prompt-alias promote route and the version-commit route — since it commits a real version and moves an alias.

Verified with apps/api running via npm run dev and a real, separately booted apps/worker process (node dist/index.js), both pointed at the same Redis instance so the API's enqueued optimize/cell/finalize/judge jobs are actually picked up by the worker.


POST /api/v1/prompts/:promptId/optimize

Kicks off an optimize attempt: validates draft_count (default 3, hard cap 6), freezes the dataset's examples, creates an experiment + a queued run, and enqueues an eval-optimize job. Returns immediately — drafting the candidates (one gateway call to the optimizer model), resolving the real (candidate + production-baseline) x model grid, and enqueuing the cell/finalize BullMQ Flow all happen out of the request path, in the optimizeWorker process (processOptimize).

Demo setup: prompt "support-reply-e6t6" v1 (production) is deliberately weak — a vague, unconstrained system message ("You are a friendly customer support agent. Help the customer with their issue: {{ complaint }}") that in practice produces multi-sentence, exclamation-mark-laden replies with greetings/sign-offs. The dataset's two examples carry criteria demanding a strict, judge-checkable format (exactly two sentences, no exclamation marks, no greeting/sign-off, a concrete next step), and its overall_feedback spells out the exact rewrite instruction the optimizer should embed in its candidates. (A first attempt with a milder overall_feedback produced a candidate that only tied production — see the "first attempt" note below — so the dataset was rebuilt with more explicit feedback and re-run.)

curl -X POST $ACRUXCORE_BASE_URL/prompts/f23200d4-22df-4d3f-a4ac-f704fc0bd2f3/optimize \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"dataset_id": "ef6525f8-4597-456b-8b17-5dc2dbb1a4f2", "models": ["gpt-4o-mini"], "draft_count": 2}'

Response (status 202):

{ "run_id": "97584f62-79ca-4575-ab68-34c6ae5ada03", "status": "queued" }

Polling GET /api/v1/runs/97584f62-79ca-4575-ab68-34c6ae5ada03 (E3's run endpoint, documented in experiments.md) a few seconds later shows the real grid the optimizer resolved — two drafted candidates plus the production baseline, all six cells (3 variants x 1 model x 2 examples) succeeded:

Response (status 200) — succeeded:

{
"id": "97584f62-79ca-4575-ab68-34c6ae5ada03",
"experimentId": "2a3fed67-2751-4adf-89cc-7ff87964fd89",
"status": "succeeded",
"startedAt": null,
"endedAt": "2026-07-08T10:08:22.466Z",
"error": null,
"createdAt": "2026-07-08T10:08:00.335Z",
"grid": [
{ "model": "gpt-4o-mini", "cellKey": "candidate-A|gpt-4o-mini", "variantKind": "candidate", "variantLabel": "candidate-A", "promptCandidateId": "612a38c2-3b3a-460c-ae01-ec4547dbc47b" },
{ "model": "gpt-4o-mini", "cellKey": "candidate-B|gpt-4o-mini", "variantKind": "candidate", "variantLabel": "candidate-B", "promptCandidateId": "c808b0a3-b95c-4dee-af1a-1cd79e766005" },
{ "model": "gpt-4o-mini", "cellKey": "production|gpt-4o-mini", "variantKind": "version", "variantLabel": "production", "promptVersionId": "bd214c90-a5e1-4e2b-8231-abda126cc739" }
],
"exampleCount": 2,
"results": { "total": 6, "succeeded": 6, "errored": 0 }
}

First attempt (documented honestly, not fabricated)

The very first optimize run against this prompt used a dataset whose overall_feedback only restated the criteria ("Replies must be exactly two sentences, no exclamation marks, and no greeting or sign-off phrases.") without giving the optimizer copyable rewrite instructions. Both drafted candidates failed to beat production — one tied it (avgScore 35 vs 35, deltaVsBaseline.label: "flat"), the other regressed (avgScore 20, label: "regressed") — because GPT-4o-mini kept writing polite, multi-sentence replies regardless of the rewritten system message's intent. This is the real, observed first-attempt behavior, not a scripted success — see GET /runs/532b1c30-247e-4327-831c-327a92d9e4c2/report for that run's full (unfavorable) numbers. The dataset was then rebuilt with explicit, literal rewrite instructions in overall_feedback (see the curl above) and re-run, producing the compelling win documented below. processOptimize itself never sends the production template's actual prior output to the optimizer on this (first) attempt for a prompt — it has no priorOutput to cite yet — so the quality of overall_feedback is what determines whether the optimizer's rewrite meaningfully changes model behavior.

Error responses

Nonexistent prompt id (status 404):

curl -X POST $ACRUXCORE_BASE_URL/prompts/00000000-0000-0000-0000-000000000000/optimize \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"dataset_id": "ef6525f8-4597-456b-8b17-5dc2dbb1a4f2", "models": ["gpt-4o-mini"]}'
{ "error": { "code": "NOT_FOUND", "message": "Prompt not found." } }

GET /api/v1/runs/:id/report (already documented — E5)

The comparison-report endpoint itself is unchanged by E6 — it is the same GET /runs/:id/report built in Phase 5 E5 and already fully documented in experiments.md, including its response shape, the deltaVsBaseline/leaderboard/winner fields, and its 404 error case. Not duplicated here. The only thing E6 changes is what can appear in variants/cells: a variantKind: "candidate" entry (an optimizer-drafted rewrite, keyed by promptCandidateId instead of promptVersionId) alongside the usual "version" baseline entry.

Verified for the winning run above — the rebuilt dataset's run genuinely demonstrates the loop working: candidate-A scores 100 (passRate 1) against production's 10 (passRate 0), a deltaVsBaseline of +90 labeled "improved", and candidate-A is the advisory winner:

curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs/97584f62-79ca-4575-ab68-34c6ae5ada03/report

Response (status 200):

{
"runId": "97584f62-79ca-4575-ab68-34c6ae5ada03",
"status": "succeeded",
"models": ["gpt-4o-mini"],
"variants": [
{ "variantKind": "candidate", "variantLabel": "candidate-A", "isProductionBaseline": false },
{ "variantKind": "candidate", "variantLabel": "candidate-B", "isProductionBaseline": false },
{ "variantKind": "version", "promptVersionId": "bd214c90-a5e1-4e2b-8231-abda126cc739", "variantLabel": "production", "isProductionBaseline": true }
],
"cells": [
{
"cellKey": "candidate-A|gpt-4o-mini",
"variantLabel": "candidate-A",
"model": "gpt-4o-mini",
"isProductionBaseline": false,
"avgScore": 100,
"passRate": 1,
"exampleCount": 2,
"scoredCount": 2,
"unscoredCount": 0,
"deltaVsBaseline": { "score": 90, "passRate": 1, "label": "improved" }
},
{
"cellKey": "candidate-B|gpt-4o-mini",
"variantLabel": "candidate-B",
"model": "gpt-4o-mini",
"isProductionBaseline": false,
"avgScore": 95,
"passRate": 1,
"exampleCount": 2,
"scoredCount": 2,
"unscoredCount": 0,
"deltaVsBaseline": { "score": 85, "passRate": 1, "label": "improved" }
},
{
"cellKey": "production|gpt-4o-mini",
"variantLabel": "production",
"model": "gpt-4o-mini",
"isProductionBaseline": true,
"avgScore": 10,
"passRate": 0,
"exampleCount": 2,
"scoredCount": 2,
"unscoredCount": 0,
"deltaVsBaseline": null
}
],
"leaderboard": ["candidate-A|gpt-4o-mini", "candidate-B|gpt-4o-mini", "production|gpt-4o-mini"],
"winner": { "cellKey": "candidate-A|gpt-4o-mini", "variantLabel": "candidate-A", "model": "gpt-4o-mini", "avgScore": 100 }
}

GET /runs/:id/cells/:cellKey (also E5, same file) confirms candidate-A's actual generated output is a genuine, judge-verified fix — e.g. for the late-delivery example: "I'm sorry to hear that your package arrived three days late and the box was crushed. I can process a replacement for you right away." (score 100, passed: true, reason: "consists of exactly two sentences ... adheres to all specified guidelines without any extraneous elements"). Production's own output for the same example scored 10 (see the cell drill-down for the exact multi-sentence, greeting-laden text the judge penalized).


POST /api/v1/runs/:id/promote

Promotes one optimizer-drafted candidate to a real, immutable PromptVersion and moves an alias (default production) to point at it — the human-in-the-loop capstone of the loop. Reuses VersionsService.commitVersion and AliasesService.promoteAlias verbatim (both keep their own audit trail), so promoting a candidate is indistinguishable, downstream, from a human manually committing that exact template. Editor role or above required.

curl -X POST $ACRUXCORE_BASE_URL/runs/97584f62-79ca-4575-ab68-34c6ae5ada03/promote \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt_candidate_id": "612a38c2-3b3a-460c-ae01-ec4547dbc47b"}'

Response (status 200):

{
"version": {
"id": "ce24f374-d40f-4853-a3a0-4bd2a467881e",
"promptId": "f23200d4-22df-4d3f-a4ac-f704fc0bd2f3",
"versionNumber": 2,
"messages": [
{
"role": "system",
"content": "You are a friendly customer support agent. Respond in EXACTLY two sentences. The first sentence apologizes and names the specific issue: {{ complaint }}. The second sentence states one concrete next step (replacement or refund). Do not use any exclamation marks. Do not include a greeting (e.g. Hi, Hello) or a sign-off (e.g. Thank you, Best regards, look forward to). Do not ask the customer for more information."
}
],
"variables": ["complaint"],
"createdBy": "03198832-644c-4a1c-bc39-8b1d38df12f1",
"createdAt": "2026-07-08T10:09:02.582Z"
},
"alias": {
"id": "ed288af9-7d03-4e1e-afd9-6ff12c5257d6",
"alias": "production",
"versionId": "ce24f374-d40f-4853-a3a0-4bd2a467881e",
"versionNumber": 2,
"updatedAt": "2026-07-08T10:09:02.594Z"
}
}

Verified the promotion actually happened, not just the response body: GET /prompts/:id/versions before -> 1 version (v1); after -> 2 versions (v2 now first/newest), and GET /prompts/:id/aliases before -> production @ v1; after -> production @ v2 (staging stays at v1, untouched):

curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/prompts/f23200d4-22df-4d3f-a4ac-f704fc0bd2f3/versions

Response (status 200) — BEFORE promote: {"data":[{"versionNumber":1,...}],"total":1,...}. AFTER promote: {"data":[{"versionNumber":2,...},{"versionNumber":1,...}],"total":2,...}.

curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/prompts/f23200d4-22df-4d3f-a4ac-f704fc0bd2f3/aliases

Response (status 200) — AFTER promote:

[
{ "id": "405c2762-b136-4057-a38e-55d179905091", "alias": "staging", "versionId": "bd214c90-a5e1-4e2b-8231-abda126cc739", "versionNumber": 1, "updatedAt": "2026-07-08T10:05:58.737Z" },
{ "id": "ed288af9-7d03-4e1e-afd9-6ff12c5257d6", "alias": "production", "versionId": "ce24f374-d40f-4853-a3a0-4bd2a467881e", "versionNumber": 2, "updatedAt": "2026-07-08T10:09:02.594Z" }
]

Error responses

Nonexistent run id (status 404):

curl -X POST $ACRUXCORE_BASE_URL/runs/00000000-0000-0000-0000-000000000000/promote \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt_candidate_id": "612a38c2-3b3a-460c-ae01-ec4547dbc47b"}'
{ "error": { "code": "NOT_FOUND", "message": "Run not found." } }

Candidate id that does not belong to this run (status 404) — team-scoped AND run-scoped, so even a real candidate id from a different run 404s:

curl -X POST $ACRUXCORE_BASE_URL/runs/97584f62-79ca-4575-ab68-34c6ae5ada03/promote \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt_candidate_id": "00000000-0000-0000-0000-000000000000"}'
{ "error": { "code": "NOT_FOUND", "message": "Prompt candidate not found for this run." } }

GET /api/v1/runs/:id/candidates/:candidateId

Reads one optimizer-drafted candidate's own template (messages) and the optimizer's rationale for it (E7 Task 5) — the read a promote-review UI needs to show WHAT is about to be turned into a real, immutable version BEFORE a human confirms POST /runs/:id/promote. Reuses OptimizeRepository.getCandidateForRun verbatim (the same team + run scoped lookup promoteCandidate already used) — no new query was written. Read-only: requireAnyAuth only, no role restriction — viewing a candidate does not require promote-right, only actually promoting it does.

Demo setup for this verification: a fresh prompt ("e7t5-greeting", v1 "Answer as {{ name }}" promoted to production) and a 2-example dataset ("Reply in exactly one word."), optimized with draft_count: 2 against gpt-4o-mini. The run resolved candidate-A + candidate-B + the production baseline, all 6 cells succeeded.

curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs/d7803330-07a7-46e0-a032-b1790989e853/candidates/b8dfffd0-b5cc-472b-8585-128223c3d753

Response (status 200):

{
"id": "b8dfffd0-b5cc-472b-8585-128223c3d753",
"promptId": "7a82ce01-0f07-4d04-b079-a05e4f7e3be9",
"messages": [
{ "role": "system", "content": "Respond with a single word: {{ name }}" },
{ "role": "system", "content": "Please provide only one word, which should be {{ name }}." }
],
"rationale": "Revised the template to emphasize that the response should consist of exactly one word.",
"label": "candidate-A",
"createdAt": "2026-07-08T11:25:57.547Z"
}

Error responses

Nonexistent run id (status 404):

curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs/00000000-0000-0000-0000-000000000000/candidates/00000000-0000-0000-0000-000000000000
{ "error": { "code": "NOT_FOUND", "message": "Run not found." } }

Real run, but a nonexistent candidate id (status 404):

curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs/d7803330-07a7-46e0-a032-b1790989e853/candidates/00000000-0000-0000-0000-000000000000
{ "error": { "code": "NOT_FOUND", "message": "Prompt candidate not found for this run." } }