is_correct) and a separate quality score (quality_score). Millwork
records these signals and uses them with the follow-up actions you allow.
You own the rules; Millwork does not independently verify that they are right
for your product.
To use a third-party evaluator, check that its endpoint accepts and returns
the formats below. You may need a handler that translates between them.
Before you begin
- Your organization is approved for the private preview.
- You have Node.js 20 or later, Bash or Zsh,
curl, andjq. - You can add a public HTTPS route to an app you run.
- You will use public information or examples created for testing.
SOLVERAPI_API_KEY in your terminal. Signing in through
the CLI’s browser window alone does not set that variable for curl.
Never paste a key into a support message or agent conversation.
Use the same terminal for the rest of this guide:
What Millwork sends
Every call is one HTTPS POST with acandidate field. For the model run in
this guide, the candidate is the model’s response as a string. Asking for
JSON does not turn that string into an object:
POST /v1/verifiers/{verifierId}/test send this reserved
candidate:
What your handler must return
Return200 with JSON. is_correct and quality_score are required on
every success response, including the probe.
A quoted
"true", a quality_score outside 0 to 1, or a non-boolean
anchor is a contract failure, not a failed check.
Transport facts
These are constraints of the call, not defaults you can override.
The URL must be
https on a public address. Do not put a secret in the URL.
Millwork does not follow redirects.
Hard rules and quality rules
Keep the two signals separate.- Hard (
is_correct): schema checks, exact matches, checksums, required fields, ordered steps, other tenant-owned deterministic assertions. - Quality (
quality_score): return a number from0to1on every successful response. A separate model judge is optional; the score is not. Calculate it with your scoring function, such as the example below. - A high quality score never turns a failed assertion into
is_correct: true. - A low quality score never turns a passed assertion into
is_correct: false. - Changing only the judge or quality function must not flip
is_correct.
Test the verdict and score separately
Try these cases through the same handler you will register:- A candidate with a missing or incorrect price and a long summary must have
is_correct: false, even if its quality score is high. - A candidate with the expected price and a short summary can have
is_correct: trueand a low quality score. - Change only the quality function. The correctness answer must stay the same.
Reserved probe handling
Treat only the exact reserved object as a probe:- If it is the reserved probe, return a valid negative result quickly:
is_correct: false,quality_score: 0. Do not call your real check. A live candidate equal to the marker must not pass a production gate. Registration and test still accept this body;readydoes not requireis_correct: true. - If
solverapi_probeappears in any other shape, fail closed. Do not take the probe shortcut. Never return a production pass because the marker was present. Returningis_correct: falsewithquality_score: 0is the closed path. - The candidate-only request cannot prove who called you. A marker must never grant a live pass.
Build the handler
Save this example asoutput-check.mjs and run it with
node output-check.mjs. It checks that a listing’s price matches the trusted
price 19.99 configured in the handler. Its quality score measures summary
length, capped at 200 characters; it does not judge the summary’s accuracy.
Replace these example rules with the checks and scoring function you use for
your own task.
https URL, not http://127.0.0.1.
Deploy and register the handler
Deploy the handler in your app and confirm that its public HTTPS URL is reachable. Millwork does not deploy it for you. Keep the route available; when you change its scoring logic, update the registeredversion and test it
again. If its authentication changes, arrange the new credential binding with
support before using it for another run.
Replace this example URL with your handler’s URL. A request key is the
value sent in the Idempotency-Key header so retrying the same request does
not repeat the work. Generate one for registration and keep it with the body:
201 with a nonempty verifier_id, hash, status,
and preflight. auth_ref: "" means this endpoint needs no authentication.
Keep VERIFIER_ID; the live request below uses it.
If the response was lost, resend the same body with the same
REGISTER_REQUEST_KEY. Reusing that key returns the stored response, including
a stored error; after correcting a rejected request, use a new key.
Test the saved check with a fresh request key. Reuse this key only to recover
this particular test request, not to make a new observation after fixing the
handler:
ready, reachable, and a validated response. The
example’s reserved probe returns is_correct: false and quality_score: 0;
that negative answer is intentional. ready means the probe had no
reachability or authentication failure. It does not certify your correctness
rules, and it does not by itself mean the response matched the required schema.
What the recorded hash covers
The hash covers the registered definition: display name, version, URL, input data classes, and scoring shape. It does not fingerprint deployed code, models, prompts, datasets, or dependencies behind a stable URL. Changing scoring logic without bumpingversion (or another hashed field) does not
necessarily produce a new hash on later receipts. Versioning is your
obligation. Repeated probes can falsify a determinism claim. They cannot
prove the absence of a language model, a cache, or mutable external state.
A queue-existence or URL-resolvability check depends on changing external
state. Determinism, correctness coverage, independence, and availability are
four different properties. Observed repeatability is not independent
verification.
Describe your check’s correctness rules
The API accepts an optionalcorrectness_declaration with
{ "method": "deterministic" } when you register or update a check. This
records your organization’s statement; Millwork does not independently verify
it. A declaration-only update changes the revision, not the definition hash.
New evaluations record the declaration in effect at that time. A receipt can
show “Tenant declared deterministic correctness; not independently verified.”
Older receipts do not gain a declaration when you add one later. See the
API reference for registration, updates, and reads.
If registration or test fails
After each correction, call
POST /v1/verifiers/{verifierId}/test again.
Do not assume a later run will retest for you.
Use the check on a live run
A live run charges a platform fee when accepted and may incur separate model usage. It needs credit and quota. Review model access and billing before continuing. The handler in this example adds no model call of its own. You need a saved model (the API calls it an arm). If you do not have one, open Run a model and complete Choose a model from your catalog and Save the model to your organization. Return here before its Submit the run step. Keep itsARM_ID in this
terminal. You do not need to run its example task.
Check the saved model and both IDs before preparing the paid request:
VERIFIER_ID; omitting that field would use a basic output-presence
check instead. on_eval: [] requests no repair or fallback action after the check.
The model-usage budget stops further calls after recorded usage reaches it; a
call already started can finish above it. The platform fee is separate.
202 with a nonempty run ID. If the response was lost,
resend the saved body with the same RUN_REQUEST_KEY. Changing that key starts
new work. If the request was refused, follow its error code in
Errors and retries before submitting again.
Wait for a final status, then fetch the receipt:
is_correct: false is still an answer from your check:
GET /v1/executions/{executionId}/events and any verification_checks on the
receipt. The receipt never includes the model text or named_metrics.
For another task, save a new body and generate a new RUN_REQUEST_KEY. Keep
the old key only for recovering the original submission.
Results and receipts
Read the run’s output and understand the evidence on its receipt.
Errors and retries
Recover a failed request without starting work twice.