---
title: Every record explains itself: the ai_context serializer
canonical: https://getcyril.com/blog/every-record-explains-itself/
published: 2026-05-20
updated: 2026-08-28
author: Sam Akbari
language: en
---
# Every record explains itself: the ai_context serializer

The exciting part of AI-native software is the agent doing something useful. The part that actually makes it work is boring: how a record describes itself to a model.

Get that wrong and every downstream demo is built on sand. Get it right and the agent stops guessing.

## Raw rows are the wrong input

A database row is built for the database. It has foreign keys, status enums, nullable columns, internal flags, and timestamps in UTC. Hand that to a model and you've asked it to do two jobs at once: reconstruct what the record *means*, and then reason about it.

It will do the first job badly. `status: 3` means nothing without the lookup table. A `null` in `closed_at` could mean "still open" or "never tracked." A foreign key is a number the model can't follow. So the model invents the meaning it needs — confidently — and the error compounds through every step that follows.

The fix isn't a bigger model. It's giving the model the right input.

## What an ai_context serializer is

In Cyril, every entity — an account, a deal, a project, a ticket, an invoice — exposes an `ai_context` serializer. It's a single method that returns a deterministic, schema-aware view of the record built specifically for AI grounding.

That view does the interpretation the model shouldn't have to:

- Enums are resolved to their human meaning — `status: 3` becomes `"stage: negotiation"`.
- Related counts are folded in — an account carries its open-ticket count, active-project status, and outstanding-invoice state, because that's what a question about the account will need.
- Internal-only fields are stripped — the model never sees row IDs, soft-delete flags, or tenant plumbing.
- The shape is stable — the same record produces the same context every time, so prompts are cacheable and behaviour is reproducible.

It is, deliberately, not the API response and not the database row. It's a third representation whose only audience is a model.

## Why "mandatory on every entity" is the whole point

It would be easy to write an `ai_context` serializer for the three entities a launch demo touches. That's the trap. The value of a single data graph is that a question can cross *any* boundary — "which at-risk accounts also have a late project and an unpaid invoice?" only works if accounts, projects, and invoices all explain themselves the same way.

So in Cyril the serializer is a requirement, not a feature. A new entity isn't done until it has one. The test patterns check for it. That discipline is unglamorous and it's exactly what lets an agent traverse the platform without hitting a record it can't read.

## Determinism is a security property too

Because the serializer is the only AI-facing view, it's also the place we control exposure. Field-level decisions about what an agent may see live in one auditable spot per entity, scoped by `org_id` like every other query. There's no separate "AI export" path quietly widening the blast radius — the same serializer that grounds the model is the same boundary that limits it.

## What it buys you

- Ask a cross-module question and get an answer grounded in resolved, related, current data — not raw rows the model had to decode.
- Trust the answer enough to act on it, because the same record always produces the same context.
- Know that what the AI can see is defined in one place per entity, not scattered across integrations.

The serializer will never be the headline feature. It's the layer that decides whether the headline features are true.

## Common questions

### How is this different from the API response?

The API response is shaped for a developer assembling a screen: it is complete, normalised, and expects the caller to know the domain. The serializer is shaped for a reader with no schema knowledge and no ability to follow a foreign key, so it resolves rather than references.

### Why not just run retrieval over the database?

Retrieval finds text that resembles the question. It answers what was written down at some point, not what is currently true of a record — and for "which accounts are at risk this quarter", currently true is the whole question.

### Does building this view slow queries down?

The related state it folds in is state a real answer would have needed anyway; gathering it once, deterministically, is cheaper than the model asking for it across several turns. Determinism also makes the result cacheable, which the raw-row path is not.

### What happens when a new entity is added?

It is not finished until it has one. That is enforced in the test patterns rather than left to memory, because the value of the graph is that a question can cross any boundary — one entity without a serializer is a hole an agent falls into.

---

If you'd like to be among the first to use Cyril, [join the waitlist](/waitlist/).
