Sprinter Docs

Unified Response System

One typed submission layer for people and agents — criteria sets define templates, entity_responses preserve versioned evidence, and promotion writes approved values into canonical record fields.

Architecture: Three Layers

The platform has three semantic layers. No exceptions, no mixing.

1. Canonical Graph (schemas, core properties, entities, relationships)

Entity schemas define the durable structure of the graph. Core properties live in entity.content and represent accepted truth. Schemas are not replaced by criteria sets — they remain canonical.

2. Collaboration Layer (criteria sets, submissions/responses, current values, rollups)

Criteria sets are reusable evaluation overlays. They define how humans and agents propose data. Submissions (entity responses) log the work — append-only, versioned, with provenance. Promotion writes accepted values into entity.content (the "current value"). Aggregation computes rollups (group meaning across many responses).

Scoring is not a separate subsystem. A score is a response with numeric/rating criteria items and rollup rules — computed at submission time and stored on the response row.

3. Presentation Layer (views, blocks)

Views and blocks render all of the above. They are strictly presentational — no storage semantics, no truth definitions.

Overview

The unified response system gives every submission — regardless of source — a single home, a review step, and an explicit promotion action that writes the value into the canonical record.

The four moving parts are:

  1. Entity schema / entity.content — the canonical typed record (current values).
  2. Criteria sets — define how a user or agent proposes data (record, assessment, or temporal templates).
  3. Entity responses — record who submitted what values when for a given entity and criteria set (append-only history).
  4. Promotion — an atomic RPC that cherry-picks a value from a response and writes it to entity.content (acceptance).

Key Concepts

Criteria Set

A criteria set is a reusable evaluation framework. Each set has a list of dimensions — the columns to collect — and optional metadata controlling display and aggregation.

interface CriteriaSetDimension {
  key: string;           // field identifier, e.g. "revenue"
  label: string;         // display label
  type: "number" | "text" | "rating" | "select" | "richtext";
  scale?: [number, number];  // [min, max] for number/rating
  step?: number;
  options?: string[];    // for select type
  weight?: number;       // 0–1, used in weighted score
  description?: string;
  fieldName?: string;    // maps to an entity content field for promotion
  required?: boolean;
}

A criteria set also has a kind:

  • record — schema-backed field capture for canonical record values
  • assessment — scoring/evaluation rubrics
  • temporal — repeated snapshots keyed by temporal_key

A criteria set can be scoped to one or more entity type slugs (entity_type_slugs) or left unscoped (available to all types). When is_default is true, the set cannot be deleted and is treated as the canonical evaluation framework for its entity type.

Auto-generation: syncDefaultCriteriaSet(tenantId, entityTypeSlug, schema, fieldConfigs) inspects the entity type's JSON schema and field configs, maps each field to a dimension type, and upserts a default criteria set with slug default-{entityTypeSlug} and kind: "record". This runs after entity type creation and update, and can also be called from background jobs.

Entity Response

An entity response is one submission event — a snapshot of values for all (or some) dimensions at a point in time.

interface EntityResponseRecord {
  id: string;
  entity_id: string;
  criteria_set_id: string;
  criteria_snapshot: CriteriaSetDimension[];
  submitted_by_user: string | null;
  submitted_by_agent: string | null;
  source: "extraction" | "manual" | "workshop" | "optimization" | "feedback";
  values: Record<string, unknown>;        // dimension key → submitted value
  field_meta: Record<string, FieldMeta>; // per-dimension confidence + sources
  weighted_score: number | null;
  normalized_score: number | null;
  status: ResponseStatus;
  promoted_fields: string[];             // which dimensions have been promoted
  reviewed_by: string | null;
  review_notes: string | null;
  parent_response_id: string | null;     // for linked follow-up responses
  temporal_value: string | null;         // for time-series snapshots
  notes: string | null;
}

criteria_snapshot preserves the exact submitted template on the row, so historical responses remain stable even if the source criteria set changes later.

Statuses: submittedpromoted / partially_promoted / superseded / rejected. A response is partially_promoted when some but not all of its dimensions have been promoted.

Scoring: computeResponseScore(values, dimensions) computes a weighted score and a 0–1 normalized score from numeric and rating dimensions that have both a weight and a scale. Scores are stored on the response row at submission time.

Promotion

Promotion is the controlled step that writes a response value into the canonical record. The promote_field_value SQL RPC is atomic: it updates entity.content and marks the field in promoted_fields in a single transaction. This prevents partial state if the caller crashes mid-way.

The promotion route requires entities.team.update, validates that the response belongs to the target record, and passes reviewer metadata into the RPC.

The server action promoteFieldValue(entityId, responseId, fieldName) wraps this RPC.

FieldMeta

Each response dimension can carry extraction provenance:

interface FieldMeta {
  confidence?: "high" | "medium" | "low";
  sources?: FieldMetaSource[];   // document, entity, web, or field references
  tokens_used?: number;
  duration_ms?: number;
}

Feedback Quality

Rejected responses can trigger feedback reruns. The rerun path scores the reviewer feedback before dispatching the next extraction task, then writes that score into both the retry prompt and feedback.context.

The score rewards concrete signals that help the next agent act without reading chat history: a specific rejection reason, the rejected value, field name, response lineage, source evidence, criteria snapshot, and field metadata.

features/responses/lib/feedback-quality.ts exposes:

FunctionDescription
scoreFeedbackQuality(input)Returns a 0-100 score, grade, present signals, and missing signals.
formatFeedbackQualitySummary(result)Formats the agent-visible Feedback quality: N/100 (...) summary used in rerun instructions.

The benchmark fixtures in features/responses/lib/response-quality-loop-benchmark.ts use this score as one metric in the broader response quality loop.

submitResponse auto-promotion respects criteria dimensions that declare extraction.requiresApproval: true. When a requested auto-promoted field maps to a requires-approval dimension in the submitted response's criteria_snapshot, finalizeEntityResponseAdmin() leaves that field submitted for review instead of promoting it into entity.content.

How It Works

Submission flow:

  1. A user fills the ResponseForm block in the workspace, or an agent calls a submission server action.
  2. submitEntityResponse() validates the values against the criteria set dimensions, runs computeResponseScore(), snapshots the criteria definition, and inserts the row with status: "submitted".
  3. ResponsesTab on the entity detail page lists all responses for the entity, grouped by criteria set, with per-row promotion controls.
  4. A reviewer clicks Promote on a dimension value. The client calls promoteField()promote_field_value RPC → entity.content is updated atomically. The response row's promoted_fields array and status are updated in the same transaction.

Field-population flow:

  1. Field-population work starts from an action/task and executes in a sessions row with ordered session_events for auditability.
  2. The produced value is submitted as an entity_responses row with source: "extraction" or the more specific agent/API source.
  3. Reviewers promote accepted response values into entity.content; rejection feedback triggers a rerun session against the same response context.
  4. Direct entity writes are reserved for explicit editor/API paths. Agent-produced values should move through responses so review, provenance, realtime, and promotion semantics stay consistent.

Aggregation flow:

aggregateEntityResponses(entityId, criteriaSetId) computes per-dimension statistics (count, mean, median, min, max, value frequency for selects) and a score progression time series. The result is a ResponseAggregation object consumed by useResponseAggregation and displayed in ScoreProgressionChart.

API Reference

Server actions (features/responses/server/actions.ts)

FunctionDescription
listCriteriaSets(opts?)List all criteria sets for the tenant. Pass entityTypeSlug to filter.
getCriteriaSet(id)Fetch a single criteria set by ID.
getCriteriaSetBySlug(slug)Fetch a criteria set by slug.
createCriteriaSet(input)Create a new criteria set.
updateCriteriaSet(id, input)Update name, dimensions, or display settings.
deleteCriteriaSet(id)Delete (blocked if is_default).
submitEntityResponse(input)Submit a new response; validates values, snapshots criteria, and computes/stores scores.
submitEntityResponseAdmin(input)Admin-client variant used by extraction/background jobs.
listEntityResponses(entityId, opts?)List responses for an entity, optionally filtered by criteria set.
getEntityResponse(id)Fetch a single response.
updateResponseStatus(responseId, status, reviewNotes?)Update review status and review metadata on a response. When status === "rejected" and reviewNotes is provided, dispatches one extraction/result-rejected Inngest event per field in the response's values, which feedbackRerun consumes to schedule an auto-rerun with the rejected value + reviewer notes as agent instructions. The event dispatch is best-effort (failures are logged, not thrown) so a reject click can never 500 on an Inngest outage.
promoteFieldValue(entityId, responseId, fieldName)Atomically promote one dimension value into entity.content.
promoteFieldValueAdmin(input)Admin-client variant used by extraction/background jobs.
aggregateResponses(entityId, criteriaSetId)Compute dimension statistics and score progression.

Auto-generation (features/responses/server/sync-default-criteria-set.ts)

FunctionDescription
syncDefaultCriteriaSet(tenantId, entityTypeSlug, schema, fieldConfigs)Upsert a default record criteria set from the entity type schema. Uses admin client — safe to call from background jobs.

Score utility (features/responses/types.ts)

FunctionDescription
computeResponseScore(values, dimensions)Returns { weighted, normalized } from numeric/rating dimensions with weight and scale. Returns { weighted: null, normalized: null } if no qualifying dimensions.
defaultCriteriaSetSlug(entityTypeSlug)Returns "default-{entityTypeSlug}".

API routes

RouteMethodsDescription
/api/criteria-sets/GET, POSTList or create criteria sets
/api/criteria-sets/[id]GET, PATCH, DELETEFetch, update, or delete a criteria set
/api/entities/[id]/responses/GET, POSTList or submit responses for an entity
/api/entities/[id]/responses/[rid]GET, PATCH, DELETEFetch, update, or delete a response
/api/entities/[id]/responses/aggregateGETFetch aggregated statistics

Client hooks (features/responses/hooks/use-entity-responses.ts)

HookDescription
useEntityResponses(entityId, opts?)React Query hook — list responses, auto-refetch on focus
useCriteriaSets(opts?)React Query hook — list criteria sets
useResponseAggregation(entityId, criteriaSetId)React Query hook — aggregated statistics

Components

ComponentLocationPurpose
CriteriaSetRenderercomponents/criteria-set-renderer.tsxDispatcher — chooses the renderer based on criteriaSet.display_type (default vertical-list vs. likert-matrix). Drop-in replacement for <FormSpecRenderer>.
LikertMatrixRenderercomponents/likert-matrix-renderer.tsxControlled Likert grid — rows = numeric dimensions, columns = scale values. Required dimensions get inline error messaging via aria-required + role="alert".
ResponseFormcomponents/response-form.tsxInline workspace block — renders a submission form driven by a criteria set. Uses <CriteriaSetRenderer> so any criteria set with display_type='likert-matrix' automatically gets the matrix UI.
ResponsesTabcomponents/responses-tab.tsxEntity detail tab — lists all responses with promotion controls
ResponseRowcomponents/response-row.tsxSingle response row with per-dimension promote buttons
FieldResponseIndicatorcomponents/field-response-indicator.tsxInline provenance control showing response coverage and promotion actions for a specific field
CriteriaSetManagercomponents/criteria-set-manager.tsxAdmin list of all criteria sets
CriteriaSetEditorcomponents/criteria-set-editor.tsxAdmin create/edit form for a criteria set
ScoreProgressionChartcomponents/score-progression-chart.tsxRecharts line chart — normalized score over time for an entity
ResponseScoreBadgecomponents/response-score-badge.tsxCompact score badge retained for response-centric surfaces.

Response Form Block

The response-form block type is registered in the block system. It renders:

  • View mode: Summary showing criteria set name, response count, and latest score.
  • Edit mode: Inline ResponseForm driven by the active criteria set. On submission, the block switches back to view mode and invalidates response queries so other blocks refresh.

The block resolver populates criteriaSet, responses, and latestScore from the entity's responses.

Display types

Criteria sets carry an optional display_type text + display_config jsonb pair. NULL = the default vertical-list renderer (one input per dimension). Set display_type='likert-matrix' for clinical instruments / diligence-scoring forms — e.g. DOC's Theralight Symptom Inventory renders as a severity matrix instead of stacked numeric fields.

Known kinds:

display_typedisplay_config shapeUse case
null (default)nullVertical list — every dimension as its own labelled input. Existing rendering behaviour.
'likert-matrix'{ scale?: [min, max], step?, anchorLabels?: { min, mid?, max } }Symptom-inventory / Likert grid. Numeric dimensions only. Per-dimension dim.scale overrides display_config.scale. Capped at 11 columns.

Per-tenant assignment lives in tenant-scoped seed scripts (scripts/seed-*.ts), not platform migrations. Add new kinds at <CriteriaSetRenderer> in features/responses/components/criteria-set-renderer.tsx — the dispatcher does Zod-validated config parsing + graceful fallback on parse failure (console.warn, falls back to default path so a misconfigured row doesn't dead-end the submit flow).

Inline Provenance

Generic field cards now render FieldResponseIndicator beneath canonical values when response history exists for that field. This exposes the record's current promoted source, lets reviewers inspect alternate submissions in context, and keeps provenance next to the field the user is actually reasoning about.

Score Surfaces

All score displays — entity detail hero, cards, lists, and data table columns — read from metadata.response_summary.latest_score via the getEntityScore() helper on a unified 0–100 scale. getEntityScore() also handles a legacy fallback: if response_summary is absent but a weighted_score key exists in entity.metadata (from old seed scripts that used a 1–10 scale), it normalizes the value to 0–100 before returning it. This fallback is retained until all entity types have been confirmed to use the entity_responses / response_summary pipeline.

scoreBadgeVariant(score) maps a 0–100 score to a badge variant: default (≥ 70), secondary (≥ 40), outline (< 40). All progress bar value props in cards, lists, and table columns receive the score directly — no division or multiplier is applied at the call site.

For Agents

submitResponse Tool

The submitResponse tool is registered in the response tool bundle and requires responses.team.create. It allows agents to submit scored responses against criteria sets without also granting direct canonical-record edit rights.

submitResponse({
  entityId: "uuid",           // required — entity to score
  criteriaSetId: "uuid",      // optional — specific criteria set
  criteriaSetSlug: "slug",    // optional — alternative to ID
  values: { quality: 8 },     // required — dimension key → value
  notes: "Assessment notes",  // optional
  source: "manual",           // optional — default "manual"
})

If the caller also has entities.team.update, workflow extraction can auto-promote the submitted fields into entity.content. If not, the response is still accepted and the tool returns promotionDeferred: true plus pendingPromotionFields, leaving canonical promotion for a later reviewer or a higher-privilege workflow agent.

Criteria set resolution: If neither criteriaSetId nor criteriaSetSlug is provided, the tool automatically resolves the default criteria set for the entity's type (the one with is_default: true).

Return value: { submitted: true, responseId, weightedScore, normalizedScore, status } on success, or { error: "message" } on failure.

The tool is defined in features/tools/response-tools.ts and wired into agent tool resolution via getResponseTools() in features/agents/resolve-tools.ts.

Note: The promoteField tool is not yet exposed. Agents can submit responses but cannot promote values into entity.content via tool — promotion requires human review through the ResponsesTab UI.

Metadata Sync

When a scored response is submitted, submitEntityResponse() automatically syncs a lightweight score summary into entity.metadata.response_summary. All consumers read scores via getEntityScore(metadata) which reads response_summary.latest_score.

The response row remains canonical; the metadata summary is a denormalized cache for list/card displays that need quick access to the latest score without joining to entity_responses.

response_summary shape

interface EntityResponseSummary {
  latest_score: number;          // 0-100 normalized
  latest_response_id: string;
  latest_criteria_set_id: string;
  latest_source: string;
  score_scale_max: 100;          // always 100 — enforced as a literal type
  updated_at: string;
}

score_scale_max is a constant sentinel value that lets future readers confirm the stored score is on the 0–100 scale without additional context.

Default Criteria Set Auto-Generation

syncDefaultCriteriaSet(tenantId, entityTypeSlug, schema, fieldConfigs) inspects the entity type's JSON schema and field configs, maps each field to a dimension type, and upserts a default criteria set with slug default-{entityTypeSlug} and kind: "record". The sync runs on entity-type create and update, and extraction can trigger it lazily if the default set is missing.

  • number / integernumber
  • booleanselect with ["true", "false"]
  • other string values → text

Default criteria sets have is_default: true and are deletion-protected. The function runs after entity type schema updates to keep the criteria set in sync.

Design Decisions

Promotion is an RPC, not a server action write. Writing to entity.content and updating promoted_fields must be atomic. A server action doing two sequential DB calls would leave the response in a bad state if the process crashed between them. The promote_field_value RPC handles both in one transaction.

Scores are computed at submission time, not on read. weighted_score and normalized_score are stored on the response row when it is created. This makes aggregation queries cheap (no recomputation) and keeps the historical score stable even if the criteria set dimensions or weights are later changed.

Default criteria sets are schema-managed. syncDefaultCriteriaSet() upserts the default record set from the entity type schema. The UI treats these sets as read-only so tenants do not make edits that will be overwritten on the next sync.

entity_type_slugs is nullable. A null value means the criteria set is available to all entity types. This supports cross-type evaluation frameworks (e.g., a common "data quality" set) without requiring an explicit list of every type.

Response history must outlive criteria-set churn. entity_responses.criteria_set_id is delete-restricted and each response stores criteria_snapshot, so historical submissions remain intact even if a criteria set is archived or later edited.

Metadata sync is a denormalized cache. Response scores are synced to entity.metadata.response_summary on submission. All readers use getEntityScore() which reads from response_summary.latest_score. Scores are always stored on the 0–100 scale; score_scale_max: 100 on the summary shape is a sentinel to make this explicit.

Legacy weighted_score retained as a normalizing fallback in getEntityScore(). Old seed data and records from before the unified response system may carry a weighted_score on a 1–10 scale in entity.metadata. getEntityScore() detects this (absence of response_summary), normalizes via ((raw - 1) / 9) * 100, and returns a 0–100 value. Migration 20260402000012 converts existing opportunity records to proper entity_responses rows; the fallback stays until all types are confirmed migrated.

featuredScore.criteriaSetId must not be NULL. Entity type config drives which criteria set is used as the featured score. If criteriaSetId is NULL, syncEntityResponseSummary() accepts any submitted response as the featured score — producing inconsistent display. Migration 20260402000012 was necessary to repair this after migration 000015 introduced a kind-mismatch bug that silently wrote NULL.

Legacy ScoringCriterion kept but deprecated. The old ScoringCriterion interface in features/entities/types.ts is retained with a @deprecated tag because existing entity type configs may reference it in their scoring.criteria array. New code should use criteria sets instead.

  • Entity Systementity.content is the target of promotion; entity types define the schema that drives auto-generated criteria sets
  • Actions — field-population work is authored as visible actions
  • Sessions — session execution produces response values and transcript evidence
  • Block Systemresponse-form is a registered block type rendered inside workspace views

On this page