Data Models Reference
This page documents the core data structures used across DecisionBox.
DiscoveryResult
A complete discovery run output. Stored in the discoveries MongoDB collection.
| Field | Type | Description |
|---|---|---|
id | string | MongoDB ObjectID |
project_id | string | Project that owns this discovery |
run_id | string | The run that produced this result. Lets a resumed run retire the partial result its previous attempt left behind, and keep that result out of its own "previously discovered" context. Absent on documents written before resume shipped, which every reader treats as "not this run". |
domain | string | Domain (e.g., gaming, social) |
category | string | Category (e.g., match3, idle, casual, content_sharing) |
run_type | string | full (all areas), partial (some areas or some failed), failed (all areas failed) |
areas_requested | string[] | Area IDs requested (empty for full run) |
discovery_date | timestamp | When the discovery ran |
total_steps | int | Number of exploration steps executed |
duration | int64 | Duration in nanoseconds |
insights | Insight[] | Discovered patterns |
recommendations | Recommendation[] | Actionable advice |
summary | Summary | Aggregate stats |
created_at | timestamp | When result was saved |
updated_at | timestamp | Last write timestamp |
Discovery logs are stored in split collections rather than embedded on the document: discovery_exploration_steps, discovery_analysis_steps, discovery_validation_results, and discovery_recommendation_log. See Discovery log collections for the rationale and the document shapes.
Insight
A discovered pattern or finding. Generated by the analysis phase.
| Field | Type | Description |
|---|---|---|
id | string | Deterministic ID: {area}-{index} (e.g., churn-1, monetization-3). Auto-generated if LLM omits it. |
analysis_area | string | Which area found this (e.g., churn, levels) |
name | string | Specific descriptive name (e.g., "Day 0-to-Day 1 Drop: 67% Never Return") |
description | string | Plain-text description with exact numbers and percentages. The raw, formatting-free form read by API consumers, list/preview UIs, and embeddings |
description_md | string | GitHub-Flavored Markdown rendition of description (emphasis, lists, small sub-headings, simple tables), rendered formatted on the insight detail view. Omitted on unformatted and pre-Markdown (legacy) insights — the dashboard falls back to description |
severity | string | critical, high, medium, or low |
affected_count | int | Number of affected users (COUNT DISTINCT user_id) |
risk_score | float64 | 0.0 to 1.0 risk assessment |
confidence | float64 | 0.0 to 1.0 confidence level |
metrics | map | Flexible key-value metrics (e.g., {"churn_rate": 0.67, "avg_sessions": 3.2}) |
indicators | string[] | Specific metric indicators (e.g., "Session drop: 12min → 4min") |
target_segment | string | Description of affected user segment |
source_steps | int[] | Exploration step numbers that support this insight |
validation | InsightValidation | Warehouse verification result (if validated) |
discovered_at | timestamp | When this insight was generated |
InsightValidation
Attached to an insight after validation by the verifier + refuter pair.
| Field | Type | Description |
|---|---|---|
verifier | StructuredVerdict | Defender-frame agent verdict with per-claim evidence rows |
refuter | StructuredVerdict | Skeptic-frame agent verdict (omitted when refuter is disabled or the refuter run failed) |
combined | string | Combined verdict: confirmed, supported, rejected, partial, unverifiable, validation_disabled, or skipped_budget_cap |
refuter_disabled | bool | True when the refuter was intentionally skipped for this document |
validated_at | timestamp | When validation was performed |
input_tokens | int | LLM input tokens consumed during validation |
output_tokens | int | LLM output tokens produced during validation |
Legacy fields (status, original_count, verified_count, query, reasoning) remain populated on documents written before the verifier+refuter pipeline shipped; the dashboard renders the new fields when present and falls back to the legacy fields otherwise.
Recommendation
An actionable suggestion based on discovered insights.
| Field | Type | Description |
|---|---|---|
id | string | Recommendation ID |
category | string | Category: churn, engagement, monetization, difficulty |
title | string | Specific action title |
description | string | Plain-text explanation with numbers. The raw form read by API consumers and previews |
description_md | string | GitHub-Flavored Markdown rendition of description, rendered formatted on the recommendation detail view. Omitted on unformatted and legacy recommendations |
priority | int | 1 (critical) to 5 (optional). P1 = highest priority. |
target_segment | string | Exact segment criteria |
segment_size | int | Number of users in the segment |
expected_impact | Impact | Expected improvement |
actions | string[] | Numbered implementation steps |
related_insight_ids | string[] | UUIDs of insights this recommendation addresses, verbatim from the same discovery's insights[].id (e.g., ["6e9261f5-c4ec-404b-bdf0-760a4644f384"]). Recommendations whose related_insight_ids cannot be resolved to an eligible insight are dropped server-side before persistence; see the RecommendationStep recommendations_dropped* counters |
confidence | float64 | 0.0 to 1.0 confidence |
Impact
Expected impact of a recommendation.
| Field | Type | Description |
|---|---|---|
metric | string | What metric improves (e.g., retention_rate, revenue) |
estimated_improvement | string | Expected improvement (e.g., "+15-20%", "+$4,975/month") |
reasoning | string | Why this improvement is expected |
If an LLM emits expected_impact as a bare prose string instead of the object (a common, model-independent behaviour), the agent coerces it into Impact{reasoning: <string>} at parse time rather than discarding the recommendation. The same tolerance applies to the numeric scalar fields: priority accepts descriptive words ("high", "critical", "P2") as well as numbers, and segment_size / confidence accept numeric strings. Providers that support schema-constrained output are additionally asked to emit the object shape at generation time.
Summary
Aggregate stats for a discovery run.
| Field | Type | Description |
|---|---|---|
total_insights | int | Number of insights generated |
total_recommendations | int | Number of recommendations generated |
queries_executed | int | Number of SQL queries executed |
errors | string[] | Error messages from failed analysis areas (if any) |
ExplorationStep
One step in the autonomous exploration phase. Drives one user-visible exploration turn — usually one LLM call, but a turn whose response can't be parsed is retried (up to a small retry budget) and every retry's token usage is summed onto the same step.
| Field | Type | Description |
|---|---|---|
step | int | Step number (1-based) |
timestamp | timestamp | When this step ran |
action | string | One of query_data, lookup_schema, search_tables, complete, complete_rejected |
thinking | string | AI's reasoning for this query |
query_purpose | string | Short description of query intent |
query | string | The SQL statement the model proposed. Not necessarily the one that produced query_result — see query_executed. |
query_executed | string | The statement that actually ran, present only when the self-healing fixer rewrote the proposal. Omitted when the proposal ran unchanged. Read through it rather than choosing between the two fields: a repair can change what the answer means (an observed run had APPROX_QUANTILES(x,4)[OFFSET(2)] rewritten to PERCENTILE_CONT(0.5) WITHIN GROUP, an approximate median for an exact one). Both are kept because they answer different questions — the proposal is the training signal, the executed statement is the only one that explains the rows. |
row_count | int | Number of rows returned |
execution_time_ms | int64 | Query execution time in milliseconds |
error | string | Error message if query failed |
fix_attempts | int | Number of applied LLM repairs for this step's query (0 if it ran cleanly on the first try or if every fix call failed). fix_attempts <= len(fix_history). |
fixed | bool | True if the query was auto-fixed after a SQL error |
fix_history | FixAttempt[] | Per-attempt fix log; omitted (or empty) when no fix was needed. Includes failed attempts (LLM error, unparseable response, filter rejection) — those carry fixer_error set. See FixAttempt. |
tokens_in | int | Input tokens consumed (sum across any parse-retry rounds on this step) |
tokens_out | int | Output tokens generated (sum across any parse-retry rounds on this step) |
FixAttempt
One entry per LLM call the self-healing SQL fix loop made for a single
exploration step. Stored as elements of ExplorationStep.fix_history in
chronological order. Every entry — including failed attempts whose
proposed SQL was never applied — is recorded so downstream tooling has
visibility into the full repair trajectory, not just the last call.
| Field | Type | Description |
|---|---|---|
step | int | The parent ExplorationStep.step number, duplicated for self-contained export. |
attempt | int | Zero-based retry index inside the step. Matches the attempt argument the executor passes to the SQL fixer. |
prompt_in | string | Fully rendered prompt the fixer sent to the LLM (system instruction + user message). |
response_out | string | Raw LLM response text — before SQL extraction. Populated even when extraction failed. |
sql_before | string | The broken SQL handed to the fixer. |
sql_after | string | The SQL the fixer proposed. Empty when the fixer failed to extract any parseable SQL (in that case fixer_error is set). |
error_in | string | The warehouse error message that triggered this fix call. |
fixer_error | string | When non-empty, the reason the proposal was NOT applied: LLM transport error, unparseable response, or post-fix security-filter rejection. When empty, the proposal was applied and the warehouse was retried with sql_after. |
input_tokens | int | Input tokens consumed by this fix call. |
output_tokens | int | Output tokens generated by this fix call. |
duration_ms | int64 | LLM call duration in milliseconds. |
timestamp | timestamp | When this fix attempt was recorded. |
The parent ExplorationStep.fix_attempts counter is the number of applied repairs (fixer_error empty), so fix_attempts <= len(fix_history) with the gap being failed-fixer rows.
AnalysisStep
Full LLM dialog for one analysis area. Captures the complete prompt and response.
| Field | Type | Description |
|---|---|---|
area_id | string | Analysis area ID (e.g., churn) |
area_name | string | Display name (e.g., Churn Risks) |
run_at | timestamp | When this analysis ran |
relevant_queries | int | Number of exploration queries used as context |
tokens_in | int | Input tokens consumed |
tokens_out | int | Output tokens generated |
duration_ms | int64 | LLM call duration in milliseconds |
insight_count | int | Number of insights extracted |
error | string | Error message if analysis failed |
RecommendationStep
Full LLM dialog for the recommendation-generation phase, stored in the
discovery_recommendation_log collection. One row per discovery run.
| Field | Type | Description |
|---|---|---|
run_at | timestamp | When the recommendation phase ran |
prompt | string | Fully rendered recommendation prompt sent to the LLM |
insight_count | int | Number of eligible insights handed to the recommender |
response | string | Raw LLM response text — before JSON cleanup |
tokens_in | int | Input tokens consumed by the LLM call |
tokens_out | int | Output tokens generated by the LLM call |
duration_ms | int64 | LLM call duration in milliseconds |
recommendations | Recommendation[] | The parsed recommendations actually persisted to the discovery (after the related_insight_ids validity check below has dropped any unresolvable ones) |
status | string | Optional observability marker. Empty on the happy path; set to skipped_no_eligible_insights when every upstream insight was rejected by the validator so the recommender was never invoked, to recommendation_parse_error when the phase produced zero recommendations because the LLM response could not be parsed, or to recommendation_figures_unresolved when every recommendation was dropped because its prose referenced figures the model never declared. The dashboard renders the reason for an empty recommendations section. |
recommendations_dropped | int | Number of recommendations the orchestrator discarded before persistence — the sum of the reason-specific counters below (recommendations_dropped_parse + recommendations_dropped_missing_ids + recommendations_dropped_unknown_id + recommendations_dropped_unresolved_figures). Omitted on a clean run. |
recommendations_dropped_parse | int | Subset of recommendations_dropped skipped because their individual JSON object could not be decoded (e.g. a field with the wrong type). Recommendations are decoded per item, so one malformed entry is dropped and counted here rather than discarding the whole batch. Omitted on a clean run. |
recommendations_dropped_missing_ids | int | Subset of recommendations_dropped that arrived with an empty or absent related_insight_ids array. Omitted on a clean run. |
recommendations_dropped_unknown_id | int | Subset of recommendations_dropped that cited at least one id not present in the eligible insight set — typically slug-style identifiers (category:severity:theme) hallucinated by the LLM in place of a real UUID. Use this counter to measure regression rates per LLM provider. Omitted on a clean run. |
recommendations_dropped_unresolved_figures | int | Subset of recommendations_dropped whose prose still carried a figure reference after the render pass — a number the model promised and never declared in its figures array, so nothing could write a checked value into the sentence. Publishing one would show a literal {{f1}} where the amount belongs. Omitted on a clean run. |
recommendation_parse_retries | int | Number of corrective re-prompts the phase issued after a response yielded zero parseable recommendations (bounded by RECOMMENDATION_PARSE_MAX_RETRIES). Omitted when zero. |
error | string | Error message if recommendation generation failed (LLM transport error or unparseable response) |
ValidationResult
Warehouse verification of an insight's claims.
| Field | Type | Description |
|---|---|---|
insight_id | string | ID of the validated insight |
analysis_area | string | Area this insight belongs to |
claimed_count | int | Count claimed by the AI |
verified_count | int | Count verified from the warehouse |
status | string | confirmed, adjusted, rejected, error |
reasoning | string | Explanation of the result |
query | string | The verification SQL query |
validated_at | timestamp | When validation was performed |
input_tokens | int | Input tokens summed across every verifier LLM call for this insight (initial verification, lookup-loop rounds, forced final round). Omitted on legacy rows. |
output_tokens | int | Output tokens summed across the same set. Omitted on legacy rows. |
DiscoveryRun
Live status of a running discovery. Stored in discovery_runs collection, updated in real-time.
| Field | Type | Description |
|---|---|---|
id | string | Run ID |
project_id | string | Project being discovered |
status | string | pending, running, completed, failed, cancelled |
phase | string | Current phase: init, schema_discovery, exploration, analysis, validation, recommendations, saving, complete |
phase_detail | string | Human-readable phase description |
progress | int | 0 to 100 percentage |
started_at | timestamp | When the run started |
updated_at | timestamp | Last status update |
completed_at | timestamp | When the run finished (null if running) |
error | string | Error message (if failed) |
steps | RunStep[] | Live step feed |
total_queries | int | Total SQL queries executed |
successful_queries | int | Queries that returned results |
failed_queries | int | Queries that errored |
insights_found | int | Insights generated so far |
attempt | int | How many times the run has been started: 1 on create, incremented by each resume. |
last_resumed_at | timestamp | When the latest attempt was requested. |
last_checkpoint_step | int | Highest exploration step with a checkpoint. > 0 on a failed run is what makes it resumable; zeroed on completion. Stamped only after the checkpoint row is durably written, so it never advertises a checkpoint that does not exist. It can still exceed the replayable prefix if an earlier write failed and a later one succeeded — replay then stops at the gap, and a resume with nothing left to replay is refused rather than silently re-exploring. |
active_ms | int64 | Cumulative active compute across attempts, so elapsed time excludes the hours a failed run sat waiting to be noticed. Recorded at each attempt's terminal write, so an attempt killed before reaching it contributes nothing — a slight undercount on hard crashes. |
lifecycle | RunLifecycleEvent[] | Append-only transition log. |
max_steps, min_steps | int | The run's own step budget, so a resume replays what the caller chose rather than the agent's defaults. |
areas | string[] | The run's own area selection. |
effort | string | The run's own effort level. |
All the fields from attempt down are optional and absent on runs that predate checkpointing — such a run reads as attempt 0 with no checkpoint, which is correct: it has nothing to resume.
RunLifecycleEvent
One transition in a run's history. A single mutable status field is enough while a run has one attempt; once a run can be resumed it shows only the LATEST attempt and silently overwrites every earlier one.
| Field | Type | Description |
|---|---|---|
status | string | The status the run entered |
at | timestamp | When |
reason | string | Free text: the failure message, or what triggered the transition |
attempt | int | Which attempt this event belongs to |
llm_provider, llm_model | string | The model that served this attempt, set on the terminal event the agent writes — which is how "which model ran which attempt" stays answerable |
RunStep
One step in the live progress feed.
| Field | Type | Description |
|---|---|---|
phase | string | Which phase this step belongs to |
step_num | int | Step number |
timestamp | timestamp | When this step occurred |
type | string | query, insight, analysis, validation, recommendation, error |
message | string | Step description |
llm_thinking | string | AI's reasoning text |
query | string | SQL query (if type=query) |
query_result | string | Query result summary |
row_count | int | Rows returned |
query_time_ms | int | Query execution time |
query_fixed | bool | Whether query was auto-fixed |
insight_name | string | Insight name (if type=insight) |
insight_severity | string | Insight severity (if type=insight) |
error | string | Error message (if type=error) |
ExplorationCheckpoint
One exploration step of an in-flight run. Stored in the discovery_checkpoints collection, keyed by run_id, and the reason a crashed run can be resumed instead of paying for its whole exploration phase again.
One document per step rather than an array on the run document, for the same reason the discovery logs were split out: an embedded array grows with run length and a long run hits the 16MB BSON limit exactly when the checkpoint matters most.
Not the same thing as discovery_exploration_steps, which is the tail-written audit log keyed by discovery_id, carries full rows and fix history, and is read by the dashboard. These rows exist only while a run is in flight, carry a bounded row sample, and are deleted when the run completes or is cancelled — with DISCOVERY_CHECKPOINT_RETENTION as the backstop.
| Field | Type | Description |
|---|---|---|
run_id | string | The run this checkpoint belongs to |
project_id | string | Project that owns the run |
step_number | int | 1-based step number; 0 is the reserved exploration-summary document |
attempt | int | Which attempt wrote this row (observability — a mixed set across a prefix is how an operator sees where the previous attempt stopped) |
kind | string | step or exploration_summary |
created_at | timestamp | TTL anchor |
| (inline) | ExplorationStep | The step itself, with its result set reduced to a bounded row sample and its fix history and raw LLM dialog stripped |
datasource_id | string | Replay argument: the datasource the action targeted |
lookup_schema | string[] | Replay argument for a lookup_schema step |
search_tables, search_top_k | string, int | Replay arguments for a search_tables step |
correlation_a, correlation_b | string | Replay arguments for a get_correlations step |
reject_reason | string | Reason class behind a complete_rejected step, so the nudge can be re-derived |
completed, completion_msg, total_steps, exploration_duration_ms | — | Summary document only (kind = exploration_summary). Its presence is what lets a resumed run skip exploration entirely. |
The summary document's attempt carries a second meaning: it identifies the exploration a resumed run is about to work over, and it is the value every AnalysisAreaCheckpoint is stamped with and filtered on.
Indexes: (run_id, step_number) unique — which makes a retried write a replace rather than a duplicate — and a created_at TTL named ttl_discovery_checkpoint.
The replay arguments are stored separately because an ExplorationStep records what came BACK (rows, counts, errors, digest) but has no field for what was ASKED — there is nowhere on it to put a lookup's table refs or a search's query text, and replay has to rebuild the model's own prior turn.
See Discovery lifecycle for what a checkpoint deliberately does not keep, and why.
AnalysisAreaCheckpoint
One completed analysis area of an in-flight run. Stored in the discovery_analysis_checkpoints collection, keyed by (run_id, area_id), and the reason a resumed run analyses only the areas its previous attempt had not finished.
A sibling collection rather than another kind inside discovery_checkpoints: that collection's unique index is (run_id, step_number) and an area has no step number, and both of its readers scan a run's rows in step order and stop the replayable prefix at the first unexpected number.
Not the same thing as discovery_analysis_steps, which is the tail-written audit log keyed by discovery_id and read by the dashboard. These rows exist only while a run is in flight and are deleted when the run completes or is cancelled — with DISCOVERY_CHECKPOINT_RETENTION, the same setting that governs the exploration checkpoints, as the backstop.
| Field | Type | Description |
|---|---|---|
run_id | string | The run this checkpoint belongs to |
project_id | string | Project that owns the run |
attempt | int | Which attempt analysed this area. The upsert is fenced on it, so an orphaned earlier attempt cannot overwrite a newer one's area |
exploration_attempt | int | The attempt that wrote the exploration summary this area was analysed over. A resumed run restores only the rows matching the exploration it is itself working over — an area's insights cite exploration step numbers, so one analysed over a different exploration would attribute its evidence to the wrong queries |
insights_validated | int | How many of the area's insights were submitted to the verifier, so a resumed run restores the per-run validation budget instead of handing the areas it still has to run a fresh one |
created_at | timestamp | TTL anchor |
| (inline) | AnalysisStep | The area itself — prompt, response, token counts, picker telemetry, its insights with their validation verdicts and evidence-quality labels, and its validation results. Nothing is stripped: an area's size is already bounded by the picker's token budget, and this is the same payload Phase 7 writes to discovery_analysis_steps |
The stored insight ids are the ones the attempt that produced the area assigned, and a resumed run does not reuse them: it mints fresh ids as it merges the area back in, and re-points the area's validation results at them. An insight id keys the embedded insight, the standalone row and the Qdrant point, so an id carried across attempts would collide with what the previous attempt had already written — see Discovery lifecycle for the three failures that follow.
Indexes: (run_id, area_id) unique — which makes re-analysing an area a replace rather than a duplicate — and a created_at TTL named ttl_discovery_analysis_checkpoint.
The inlined AnalysisStep supplies area_id, so there is no separate field for it; the key half of the unique index comes from the step itself.
A row exists only for an area that produced a parsed result, finished its validation and had its evidence-quality labels attached. An area whose analysis failed carries no row, so a resume retries it — see Discovery lifecycle for why that distinction is what makes a deadline-killed run recoverable.
Feedback
User feedback on insights, recommendations, or exploration steps.
| Field | Type | Description |
|---|---|---|
id | string | Feedback ID |
project_id | string | Project ID |
discovery_id | string | Discovery run ID |
target_type | string | insight, recommendation, exploration_step |
target_id | string | ID of the rated item |
rating | string | like or dislike |
comment | string | Optional comment (typically with dislikes) |
created_at | timestamp | When feedback was submitted |
Project
Project configuration. Stored in projects collection.
| Field | Type | Description |
|---|---|---|
id | string | MongoDB ObjectID |
name | string | Project name |
description | string | Project description |
domain | string | Domain (e.g., gaming, social) |
category | string | Category (e.g., match3, idle, casual, content_sharing) |
warehouse | WarehouseConfig | Data warehouse configuration |
llm | LLMConfig | Legacy single-provider LLM configuration. Still written, as the mirror of the default layer, so an agent image older than the API reads a truthful setup. Resolve the live value through layer_models. |
blurb_llm | LLMConfig | Legacy table-description override. The mirror of the table_descriptions layer, on the same terms. Cleared when that layer equals the default. |
llm_providers | LLMProviderEntry[] | The project's named LLM connections. Absent on a project that has never been configured for per-layer models — the accessors then synthesise a default entry from llm (and a blurb entry from blurb_llm) at read time, so no backfill is needed to READ an old document. |
layer_models | map[string]LayerModel | Which connection and model each layer uses, keyed by layer, plus the reserved default row every layer without one inherits. Non-empty means authoritative: blurb_llm is not consulted, so resetting a table-description override cannot rediscover the stale mirror. |
llm_config_version | int | The per-layer-configuration generation this project has been configured for. 0 (or absent) means never: discovery is refused and the dashboard sends the user to settings. |
profile | map | Domain-specific profile (from JSON Schema form) |
prompts | ProjectPrompts | Per-project prompt overrides |
status | string | Project status |
last_run_at | timestamp | When the last discovery ran |
last_run_status | string | Last run result |
created_at | timestamp | When the project was created |
updated_at | timestamp | Last update |
WarehouseConfig
| Field | Type | Description |
|---|---|---|
provider | string | Provider ID: bigquery, redshift |
project_id | string | GCP project ID (BigQuery) |
datasets | string[] | Dataset/schema names |
location | string | Data location |
filter_field | string | Multi-tenant filter column |
filter_value | string | Multi-tenant filter value |
config | map | Provider-specific key-value config |
LLMConfig
| Field | Type | Description |
|---|---|---|
provider | string | Provider ID: claude, openai, ollama, vertex-ai, bedrock |
model | string | Model identifier (free text) |
config | map | Provider-specific key-value config (e.g., project_id, location for Vertex AI) |
LLMProviderEntry
One named connection. config holds connection fields only; model-scoped
settings live on the LayerModel that uses it.
| Field | Type | Description |
|---|---|---|
id | string | Stable id within the project. The reserved default is the entry every layer inherits and the only one an older agent image can read credentials for, so it is a slot that gets rewritten rather than a pointer that gets re-aimed; blurb is the table-description entry's reserved id. Letters, digits, underscores and hyphens, at most 32 characters — the composed cloud secret name is length- and charset-limited. |
label | string | Human-readable name shown in the model pickers |
provider | string | Provider ID, as in LLMConfig |
config | map | Connection fields: region, base_url, auth_method, project_id, custom TLS, … |
LayerModel
One layer's choice. config is merged over the provider entry's config.
| Field | Type | Description |
|---|---|---|
provider_id | string | The entry this layer uses. Empty is read as default. |
model | string | Model id. May be empty only when an endpoint_id is set — an endpoint serves its own model and must never be sent another layer's. |
config | map | Model-scoped settings: max_input_tokens, max_output_tokens, wire_override, endpoint_id, reasoning_enabled, prompt_cache |
Next Steps
- API Reference — Endpoints that return these models
- Configuration Reference — Environment variables