Skip to main content
evaluate() is the right entry point for 95% of cases. For the other 5% (wiring evaluations into an existing pipeline, streaming datapoints from a long-running job, scoring production traffic after the fact) use the lower-level LaminarClient.evals API, or call the HTTP endpoints directly if you’re not on a Laminar SDK.

When to use this

Reach for the manual API when you need to:
  • Create an evaluation now and append datapoints to it over hours or days, as work completes.
  • Register a datapoint in the UI before the executor runs, so a row is visible while the run is still in progress.
  • Run the executor in one process and write scores from another (for example, an async judge that posts results back later).
  • Score production traces without re-running the call: save the executor output and scores against a new datapoint.
  • Write evaluations from a language or runtime with no Laminar SDK. The SDK methods below are thin wrappers over four HTTP endpoints you can call directly: see the HTTP API reference.
If none of those apply, use evaluate().

The three-phase pattern

The manual API is designed around three distinct moments in an evaluation’s lifecycle. Call them in order:
  1. Create the evaluation with create_evaluation / create. Returns an eval_id. Do this once per run.
  2. Pre-register each datapoint with create_datapoint. Returns a datapoint_id. A row appears in the UI immediately, even though the executor hasn’t run yet.
  3. Update the datapoint with update_datapoint: link it to a trace, then (once the executor and evaluators finish) write the executor output and scores.
Pre-registering is the pattern that matters. It’s how a long-running evaluation stays observable while it runs, and how a separate scoring process can write results back to rows created by the executor. Each phase is one HTTP call underneath, so the same lifecycle is available without an SDK. See the HTTP API reference for full request and response shapes.

Setup

Build executor and evaluator spans

Wrap the executor and each evaluator in observe() with the matching spanType. The evaluation UI uses EXECUTOR and EVALUATOR to know which spans hold the input, output, and score for each row.

Create the evaluation and datapoints

Open the evaluation up front, then loop over the test data. For each row, pre-register the datapoint, run the executor inside an EVALUATION span, and write scores back once the evaluators finish.
The two SDKs link traces to datapoints in slightly different places. TypeScript accepts traceId on createDatapoint only, so call it from inside the EVALUATION span and pass Laminar.getTraceId() there. Python’s update_datapoint accepts trace_id, so you can register the datapoint before the span opens and link the trace once it’s running. The Python pattern is what you want when the row needs to be visible before you know which trace will own it.

Renaming or re-tagging a finished run

A long-running job often doesn’t know everything about itself when the run is created: the final status, the git SHA it ends up testing, how many rows it processed. update (TypeScript) / update_evaluation (Python) writes the evaluation’s name and metadata after the fact, any time after create:
Fields you leave out are unchanged, so you can rename a run without touching its metadata. When you do send metadata, it replaces the stored object wholesale rather than merging: include the keys you want to keep, as the examples above do. The group cannot be changed.

Decoupling the executor from the scorer

Because update_datapoint can be called any time after create_datapoint, executor and scorer can live in different processes. A common shape:
  1. A worker runs the agent, produces a trace, and calls update_datapoint with executor_output and an empty scores={} dict.
  2. A judge process reads executor_output from the dataset or from the agent’s output store, scores it, and calls update_datapoint again with the filled-in scores.
Both writes target the same datapoint_id. The UI updates in place each time.

Backfilling without running the executor

For pure backfills (rows you already have outputs and scores for, no live executor), loop create_datapoint + update_datapoint over the pre-scored rows:
Over HTTP a backfill skips the update phase entirely: POST /v1/evals/{eval_id}/datapoints accepts executorOutput and scores on each point, so fully-scored rows land in one batched call. No EVALUATION span is opened, so no trace is attached. The row shows data, target, executor_output, and scores, which is enough for the list view, progression chart, and side-by-side comparison. Use this when you want the numbers in Laminar but don’t need per-row transcript drill-down.

HTTP API reference

All four endpoints authenticate with a project API key as a Bearer token and take a JSON body. Base URL is https://api.lmnr.ai on Laminar Cloud, or your app-server’s HTTP origin (including port) when self-hosting. Headers All request and response field names are camelCase. A bad or revoked key returns 401. The examples below assume:
Spans are the one thing these endpoints don’t carry: emit them with any OpenTelemetry SDK and pass that trace’s id as traceId on the datapoint to get per-row transcripts.

Create an evaluation

POST /v1/evals
Returns 200 with the created evaluation:
Keep the id: every later call is scoped to it.

Update an evaluation

POST /v1/evals/{eval_id}
Returns 200 with the updated evaluation in the same shape as create, or 404 when the id doesn’t exist in the project. Fields you leave out are unchanged, so you can rename a run without touching its metadata. metadata is replaced wholesale rather than merged: send the whole object, including keys you want to keep. groupId cannot be changed.

Add datapoints

POST /v1/evals/{eval_id}/datapoints
Returns 200 with the evaluation id.
Two fields the SDKs fill in for you and the API does not:
  • groupName is not inherited from the evaluation. It defaults to default on every request, so if you created the evaluation with a groupName, send the same value on each datapoints call. Otherwise the rows land in a different group and won’t appear in the group’s progression chart.
  • index is not auto-incremented. Every point you send without one gets 0. Since comparing runs matches rows across runs on index, leaving them all at 0 makes each row match every row in the other run instead of its counterpart. Number your points as you build the batch.

Update a datapoint

POST /v1/evals/{eval_id}/datapoints/{datapoint_id}
Returns 200 with the datapoint id. Scores are merged into the existing scores, so a second call adding a new score name keeps the earlier ones. Call it as many times as you like against the same datapoint_id; the UI updates in place.

Result

Manual evaluations show up in the same evaluations list, progression chart, and comparison UI as evaluate() runs. Groups, per-datapoint deltas, and CSV export all work the same way.
Evaluation detail page for Manual capitals eval with length_ok averaging 1.00 across six datapoints

Manual evaluation detail page. Progression chart and datapoint table match what evaluate() produces

Clicking a row opens the transcript for that datapoint’s trace, with the full EVALUATION root, EXECUTOR, and EVALUATOR nesting you’d expect from evaluate().
Manual evaluation trace with EVALUATION, EXECUTOR, gpt-5-mini, accuracy, and length_ok spans

One datapoint's transcript: EVALUATION root, executor, the gpt-5-mini call, and accuracy / length_ok scores

Next steps

Quickstart

The high-level evaluate() API, which is the right starting point for most cases.

Compare runs

Group manual runs so you can compare them like any other evaluation.

Concepts

The datapoint / executor / evaluator / group model the manual API maps onto.

SDK reference

Full parameters for LaminarClient.evals methods.