Hosted prompts
Host your prompt as a versioned object per call type — fetch it from preflight or the SDK, or let the gateway apply it when proxying. A prompt we host is a prompt we can manage.
Your prompt is the instruction half of what a model sees. Hosted prompts let you store it in ultrarouter as a versioned object per call type, instead of hard-coding it in your application. A prompt we host is a prompt we can manage: it can be versioned, rolled back, and — later — proposed by the flywheel behind the same guardrails as every other change.
A hosted prompt version is an instruction segment (trust class
authoritative). It is a different object from the learned knowledge module
(the capped data slot the Flywheel injects): two objects, one delivery channel.
The prompt version carries the instructions your app wants followed; the learned
module is reference data the model may consult. They never confound each other.
The version object
Each version is immutable and append-only: once created, its template text never changes. A "rollback" is re-activating an earlier version — the active pointer moves; a version body is never edited. Every version records:
- a monotonic
versionnumber per call type, - the
templatetext with{{name}}variable slots, - the declared
variables(eachnameplus arequiredflag), - a
content_sha256— the byte identity of the template (this is what shows up in the audit trail, never the template text itself), - its
source(humanfor anything you write through the API).
Managing versions
All of these use your config:write scope (or a config.write session role);
reads use config:read. A viewer role is read-only.
POST /v1/call-types/{id}/prompts # create a new (inactive) version
GET /v1/call-types/{id}/prompts # list versions + the active pointer
GET /v1/call-types/{id}/prompts/{version} # one version
POST /v1/call-types/{id}/prompts/{version}/activate # activate (or roll back to) it
POST /v1/call-types/{id}/prompts/deactivate # back to code-ownedCreating a version does not activate it. Activation writes a new call-type config version and moves the active pointer; because the pointer lives in the versioned config, the change is visible to the gateway and to preflight within one config cache window. Activating an older version is exactly how you roll back. Every mutation writes an audit event carrying the content sha (never the text).
Two ways to deliver
The active version is delivered one of two ways, and the choice is yours per call type:
- You fetch it.
POST /v1/preflightreturns the active version asprompt: { version, template, variables, sha256 }when one is active (elsenull). The prompt is part of the preflightETag, so a new activation moves theETag. Prepend the template as your own leading system instruction. - The gateway applies it. Activate with
{ "gateway_apply": true }. When you proxy a chat completion, the gateway renders the template and makes it the leading system instruction, replacing your request's leading system message(s). The learned knowledge module, if any, still lands after that stable prefix through the same one placement rule.
Code-owned is always available: with no active version — or an active version that
is not gateway_apply — a proxied request's own messages pass through untouched.
Variables
A template references {{name}} slots. When the gateway applies a hosted prompt,
you supply values through the request's ur.prompt_vars map:
{
"model": "openai/gpt-4o-mini",
"messages": [{ "role": "user", "content": "..." }],
"ur": { "prompt_vars": { "role": "support", "brand": "Acme" } }
}A missing required variable is a named, pre-dispatch
prompt_variables_missing error that lists the names — never a silent blank. A
missing optional variable renders as the empty string.
Testing a draft before it goes live
A version you create but do not activate is a candidate: your own draft,
offered for your own evidence to test before it can serve a single request. A
candidate is screened offline on the same path the Flywheel uses for every
other change — nothing is fitted to your eval data (you wrote the instruction),
so every eligible clustered item from your consented traffic is a screen item.
The screen replays each item twice: once with the candidate's rendered
instruction as the leading system message, once with the currently-active version
(or your own messages, if the call type is code-owned). Both arms are scored by
the call type's qualified evaluator suite — the same ruler that qualifies
every other candidate — and the candidate parks at screened only when it
measurably beats the baseline.
Only your own drafts are candidates. The active version is the baseline it is
measured against, never a candidate itself. A version whose source is
flywheel is refused everywhere with a named reason: a learned prompt variant
would come from a prompt proposer, which is not built — so no prompt is ever
proposed or auto-written on your behalf in this release.
A screened candidate appears in your Improvements inbox with its verdict and a "Your draft" chip, told apart from correction- and eval-derived candidates only by its provenance. The diff travels as ids and content hashes — the template text is readable only through the prompt read route above, never in a log or an audit diff.
GET /v1/flywheel/prompt-candidates # list your drafted candidates + verdicts
POST /v1/flywheel/prompt-candidates/{runId}/decision # approve (activate) or reject ("did not help")Listing candidates uses config:read (or a flywheel.read session role).
Approving or rejecting is a human step: it needs a signed-in session with the
flywheel.write role — never an API key. Approve activates that version
through the same atomic path as .../activate above (a new config version, a new
epoch), so the gateway and preflight serve your approved draft within one config
cache window. Reject records the "did not help" reason and archives the
candidate with its evidence. One lever family per call type may be in
confirmation at a time: while the Flywheel is confirming a learned-knowledge
change for a call type, a prompt candidate may still be screened but not approved
until that confirmation closes — the inbox shows the named reason.
The Flywheel
How ultrarouter turns the outcomes your product already generates into tested, customer-owned improvements — the six channels, the loop, and the laws that keep every claim honest.
The Improvements inbox
One inbox, one lifecycle for every improvement the Flywheel proposes — a prompt version, a model or routing change, an evaluator change — each with its offline result, live status and where it came from. Approve or reject with a reason; a refused one is archived with its evidence and never re-proposed.