JuliusBrussee/caveman · error
recordOutcome: values must be a non-empty plain object
Error message
recordOutcome: values must be a non-empty plain object
What it means
recordOutcome sends outcome values plus evidence references to the Caveman control API, and it validates input client-side before the request. values must be a plain object (not an array, null, class instance, or primitive) with at least one key; an empty object would produce an outcome with no measurements, which the API rejects, so the SDK fails fast with this message.
Source
Thrown at packages/mastra/src/index.ts:650
*
* Fails closed BEFORE any network call on a missing task id, a missing contract,
* empty values, an evidence map that is empty or over the server's limit, a
* confidence outside [0,1], or values that are not plain JSON. Exactly one
* request is made — no retries — and a non-2xx response throws a
* {@link CavemanOutcomeError} carrying the status and body verbatim.
*/
export async function recordOutcome(
client: CavemanOutcomeClient,
input: RecordOutcomeInput,
): Promise<RecordOutcomeResult> {
const controlApiUrl = requireBaseUrl(client.controlApiUrl, "recordOutcome: controlApiUrl");
const token = requireNonEmpty(client.token, "recordOutcome: token");
const projectId = requireNonEmpty(client.projectId, "recordOutcome: projectId");
const taskId = requireNonEmpty(input.taskId, "recordOutcome: taskId");
const contract = requireNonEmpty(input.contract, "recordOutcome: contract");
if (!isPlainObject(input.values) || Object.keys(input.values).length === 0) {
throw new Error("recordOutcome: values must be a non-empty plain object");
}
if (!isPlainObject(input.evidence)) {
throw new Error("recordOutcome: evidence must be a plain object");
}
const evidenceRefs = Object.entries(input.evidence).map(([key, value]) => {
if (!key.trim()) throw new Error("recordOutcome: evidence keys must be non-empty");
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
throw new Error(`recordOutcome: evidence.${key} must be a string, number, or boolean`);
}
if (typeof value === "number" && !Number.isFinite(value)) {
throw new Error(`recordOutcome: evidence.${key} must be a finite number`);
}
const ref = `${key}:${String(value)}`;
if (!ref.slice(key.length + 1)) throw new Error(`recordOutcome: evidence.${key} must be non-empty`);
return ref;
});
if (evidenceRefs.length === 0) {
throw new Error("recordOutcome: evidence must contain at least one reference");View on GitHub (pinned to 27d5a3981a)
Solutions
- Ensure values contains at least one scalar measurement, e.g. { correctness: 1 }.
- Skip the recordOutcome call entirely when there is nothing to report rather than sending an empty object.
- Use a plain object literal (not a class instance or array) for values.
Example fix
// before
await recordOutcome(client, { taskId, contract, values: {} });
// after
if (Object.keys(values).length > 0) {
await recordOutcome(client, { taskId, contract, values });
} Defensive patterns
Strategy: validation
Validate before calling
function isPlainObject(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null && !Array.isArray(v)
&& Object.getPrototypeOf(v) === Object.prototype;
}
function validValues(v: unknown): boolean {
return isPlainObject(v) && Object.keys(v).length > 0;
} Type guard
function isPlainObject(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null && !Array.isArray(v)
&& Object.getPrototypeOf(v) === Object.prototype;
} Try / catch
try {
await recordOutcome(client, input);
} catch (e) {
if (e instanceof Error && e.message.startsWith("recordOutcome: values")) {
logger.warn("skipping outcome with no values", { taskId: input.taskId });
return; // empty values are not worth a retry
}
throw e;
} Prevention
- Skip recordOutcome when the measurement map is empty instead of sending {}.
- Build values as object literals, not arrays or class instances.
- Unit-test payload construction at the boundary so empty objects never reach the SDK.
When it happens
Trigger: recordOutcome(client, { taskId, contract, values: {} }) or values: [] / values: null / values built from Object.create(null)-adjacent or class instances that fail isPlainObject.
Common situations: Dynamically building values from a loop that produced no entries, spreading an optional config that is undefined, or passing a Map/array because the payload schema was misread.
Related errors
- recordOutcome: evidence must be a plain object
- recordOutcome: evidence keys must be non-empty
- option not found
- cave_harness_adapter_version_invalid
- cavemanExporterConfig: protocol ${protocol} is not accepted
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/b11134bfc3f2a395.
Report an issue: GitHub.