Outcomes & Calibration API
This is the closed-loop learning surface — the decision → action → outcome causal chain plus the online calibration it drives. It is what keeps prediction confidence honest: recording an outcome re-weights the confidence of every pattern/prediction the decision was informed by (a Beta-Binomial posterior).
Decisions and outcomes are stored as decision / outcome
memories, so retrieval, embeddings, and
the GDPR subject cascade cover them for free. The surface is domain-agnostic:
subject / decision / action / outcome / reward only, never a consumer-domain
noun.
All routes are under /api/v1/projects/{projectId}/lattice.
Record a decision
POST /lattice/decisions
Log a choice an actor made, with provenance links to the patterns/predictions that informed it (credit assignment).
{
"subject": { "kind": "customer", "externalId": "acct-42" },
"actor": "policy:winback-v1",
"decisionType": "outreach",
"actionType": "send_message",
"informedBy": [
{ "memoryId": "mem_pattern_…", "refType": "pattern", "weight": 1.0 }
],
"status": "executed",
"idempotencyKey": "winback-acct-42-2026-07-13"
}refType is pattern | prediction | observation | collective. Re-sending
the same idempotencyKey returns the existing record rather than creating a
duplicate. The full DecisionRecord (with decisionId) comes back.
Record an outcome
POST /lattice/outcomes
Log the realized result. On write, it folds into the calibrated confidence of
every ref the decision was informedBy.
{
"decisionId": "dec_…",
"subject": { "kind": "customer", "externalId": "acct-42" },
"outcomeType": "conversion",
"result": "success",
"reward": 49.0,
"attributionWindowSecs": 604800,
"idempotencyKey": "outcome-dec-…"
}result is success | failure | partial. The response surfaces exactly
which refs moved:
{
"outcomeId": "out_…",
"updates": [
{
"refId": "mem_pattern_…",
"refType": "pattern",
"priorConfidence": 0.70,
"posteriorConfidence": 0.74,
"hits": 8,
"misses": 3
}
]
}List outcomes
GET /lattice/outcomes?subjectKind=…&subjectExternalId=…&decisionType=…&actionType=…&limit=…
Lists recorded outcomes for a subject (omit the subject params for scope-wide),
newest first. Each row is denormalized with the linked decision’s decisionType
and actionType.
What worked
GET /lattice/effectiveness?groupBy=…&minSupport=…
Roll up “what worked” per group. groupBy is action_type | decision_type |
policy | pattern_kind.
{
"rows": [
{
"groupKey": "send_message",
"n": 120,
"successRate": 0.41,
"avgReward": 18.7,
"confidence": 0.40
}
]
}confidence is the Beta-Binomial posterior mean of the success rate;
minSupport filters out thin groups.
Calibration report
GET /lattice/calibration?bucketCount=…
Read-only reliability report: active patterns bucketed by stated confidence, with the realized hit-rate of their past predictions per bucket. Answers “of the patterns we rate ~0.8, do ~80% of their predictions actually fire?”
{
"buckets": [
{
"lower": 0.6, "upper": 0.8,
"patterns": 14,
"predictions": 52, "hits": 33, "misses": 19,
"realizedHitRate": 0.63,
"hasData": true
}
],
"totalPatterns": 41,
"totalPredictions": 190
}bucketCount defaults to 5, clamped to [1, 20]. hasData is false for a
bucket where no prediction has been scored yet — don’t read realizedHitRate
when it’s false.
Calibration is the read side; the decisions → outcomes loop above is the write side that feeds it. Together they are the “with confidence, honestly” half of the prediction trust layer.