Reference
Read contract views
Use runs_v1 to join run status and cost with your application’s SQL data. It is
an opt-in, versioned, read-only view. Use GET /api/runs for HTTP access; avoid
coupling reports to Tracon’s internal table structure.
flowchart LR
accTitle: Where runs_v1 sits
accDescr: Tracon's store layer writes the runs table; the runs_v1 view reads it and is queried either by your own raw SQL or by an EF Core keyless entity, alongside the existing HTTP API path for writes and paged reads.
S["Tracon's store layer<br/>(writes)"] --> T["runs table"]
T --> V["runs_v1 view<br/>(read-only, versioned)"]
V --> Q["Your own SQL"]
V --> E["EF Core keyless entity"]
T --> A["GET /api/runs<br/>(paged, HTTP)"]
What the view is
Section titled “What the view is”runs_v1 is a plain SQL view over the runs table, published as an opt-in
migration set on all three SQL providers: PostgreSQL, SQL Server, and SQLite.
builder.AddTracon() .UsePostgreSql(o => { o.ConnectionString = connectionString; o.EnableReadViews = true; });The equivalent setting exists on TraconSqlServerOptions.EnableReadViews and
TraconSqliteOptions.EnableReadViews. It defaults to false: a deployment
that never turns it on pays nothing for it, and never sees a runs_v1 object in
its database.
| Provider | Object name |
|---|---|
| PostgreSQL | {schema}.runs_v1 (default schema tracon) |
| SQL Server | {schema}.runs_v1 (default schema tracon) |
| SQLite | {prefix}runs_v1 (default prefix tracon_, no separating dot) |
Columns
Section titled “Columns”| Column | Type | Notes |
|---|---|---|
run_id |
uuid / uniqueidentifier / text | |
tenant_id |
text | Not filtered — see It is not a tenant boundary |
agent_name |
text | |
session_id |
text | NULL for a run with no session |
status |
smallint | The numeric RunStatus value; stable, never renumbered |
status_name |
text | The RunStatus name ("Completed", "Failed", …) so you never hand-write the mapping |
started_at, completed_at |
timestamp | UTC; completed_at is NULL while a run is in progress |
is_streaming |
boolean | |
input_tokens, output_tokens, cached_input_tokens, reasoning_tokens, total_tokens |
bigint | cached_input_tokens is counted inside input_tokens, not in addition to it |
input_cost, output_cost, cached_input_cost |
decimal | The raw priced terms; NULL when pricing is undefined |
total_cost |
decimal | See Reading total_cost below |
cost_currency |
text | Populated whenever total_cost is |
error_type |
text | The raw error type string; NULL for a run that has not failed |
model_provider |
text | The provider that actually answered; NULL for a row written before this column existed |
input_price_per_mtok, output_price_per_mtok, cached_input_price_per_mtok |
decimal | The unit price (per million tokens) applied when the run completed — a price snapshot, NULL when the price was unknown. Rates, not amounts: they are never part of total_cost |
Everything here is metadata or a value derived from metadata. No conversation
content, tool argument, tool result, or file content is ever in scope for this or
any future view — those columns are protected at rest and stay reachable only
through the HTTP API and IRunStore.
Reading total_cost
Section titled “Reading total_cost”total_cost sums every priced term (input_cost, output_cost,
cached_input_cost) with one rule that matters: NULL is not zero.
- If pricing was never resolved for the run (an unpriced code agent, for example),
all three terms are
NULLandtotal_costisNULL— the run has no known cost, which is a different fact than “this run cost nothing.” - If at least one term is populated,
total_costsums the populated terms.
This is the exact value IRunStore.GetStatisticsAsync reports for the same run —
querying the view and reading the API give you the same number, because both read
the same underlying columns with the same null-preserving rule.
It is not a tenant boundary
Section titled “It is not a tenant boundary”runs_v1 carries tenant_id as a plain column. It does not filter by tenant.
A query against the view with no WHERE tenant_id = ... clause returns every
tenant’s rows, in a single multi-tenant deployment. Add your own tenant filter —
the same way you would against any other multi-tenant table you own.
EF Core: a keyless entity
Section titled “EF Core: a keyless entity”Map the view as a keyless entity — it has no primary key you should rely on for change tracking, and you are not writing through it:
public sealed class TraconRun{ public Guid RunId { get; set; } public string TenantId { get; set; } = default!; public string AgentName { get; set; } = default!; public string? SessionId { get; set; } public short Status { get; set; } public string StatusName { get; set; } = default!; public DateTimeOffset StartedAt { get; set; } public DateTimeOffset? CompletedAt { get; set; } public bool IsStreaming { get; set; } public long? InputTokens { get; set; } public long? OutputTokens { get; set; } public long? CachedInputTokens { get; set; } public long? ReasoningTokens { get; set; } public long? TotalTokens { get; set; } public decimal? InputCost { get; set; } public decimal? OutputCost { get; set; } public decimal? CachedInputCost { get; set; } public decimal? TotalCost { get; set; } public string? CostCurrency { get; set; } public string? ErrorType { get; set; }}
modelBuilder.Entity<TraconRun>() .HasNoKey() .ToView("runs_v1", "tracon");Three things to keep in mind:
.ToView(...)never appears in your owndotnet ef migrations addoutput. Tracon’s migration creates the view; your own migration history stays untouched. This is why the two migration steps never conflict — see Two migration steps, one order that matters less than you think.- Your own query still needs the tenant filter — the view does not apply one (above).
- Retention deletes rows out from under the view. If a retention policy
deletes an old run, it disappears from
runs_v1too — the view reflects the table’s current contents, not a separate archive.
The compatibility rule
Section titled “The compatibility rule”runs_v1 is a published contract: once shipped, it never loses a column,
renames a column, or narrows a column’s type. Adding a column is always safe and
does not require a new version. A change that would break an existing consumer
ships as runs_v2 instead, and runs_v1 keeps working for at least one major
version afterward.
Read next
Section titled “Read next”- Two connection planes — EF Core and Tracon — sharing a connection pool, and what an EF
DbContextdoes not get - Observability — run statistics through the HTTP API, the same numbers this view reads
- Choose a migration strategy —
AutoApplyMigrationsand runningtracon migrateas its own deploy step