DEV Community

Takashi Matsuyama
Takashi Matsuyama

Posted on • Originally published at blog.tak3.jp

Write Database Meaning Your AI Agent Can Actually Use — a Practical Guide to COMMENT ON

In the Kozou introduction — Kozou being an open-source tool that hands your PostgreSQL database's meaning to an AI agent over MCP — I made a claim: the place to write that meaning already exists in PostgreSQL, in COMMENT ON and view definitions, so write the meaning into the DB first. An AI agent that only sees the raw DDL is good at reading column names and types, but it doesn't know which column is a trap or which view is authoritative. So it's plausibly wrong.

This post is the how. How do you write that meaning so an agent can actually use it? The answer isn't in column names or types; it's in COMMENT ON and view definitions, written to a small set of conventions. The examples come from the demo schema (a small online store) bundled with Kozou's quickstart — the real thing, verbatim. Everything shown here you can do on plain PostgreSQL, before adopting Kozou at all; how the written meaning then reaches the agent is the last section.

Write meaning at the start of a line

Inside COMMENT ON, you write convention tags at the start of a line. Three carry the meaning: @ai / @policy / @example. There's also @widget (for the Admin UI) and @expose (for functions), but neither is about handing meaning to an agent, so this post leaves them aside.

Tag Purpose Where it goes Covered here
@ai Instructions/background for the AI agent (free text) table / column / view / foreign key / function ◎ central
@policy A business rule, recorded (advisory — not enforced) same ◎ central
@example An executable example query attached to a view view
@widget Admin UI input control for a column column mentioned only
@expose Expose a function as an RPC action function mentioned only

There's a bit of craft to it. Dull, but skip it and the tag won't fire.

  • Write it at the start of a line. A known tag (@ai, @policy, …) placed mid-line is not parsed as a tag; it stays as part of the surrounding prose (Kozou warns about it).
  • One tag per line for @ai / @policy. If you have several points, stack several @ai: lines. The only note allowed to span multiple lines is an @example block.

One more part of the convention, worth knowing on a multilingual team: the note body can be in any language. The tag name (@ai, …) and the colon after it are ASCII, but the text after the colon is kept as you wrote it — trimmed of surrounding space, but never summarized or paraphrased — so your notes can be in English, Japanese, German, whatever your team reads and the agent understands. One caveat if you write in a language with fullwidth punctuation (Japanese, for instance): the tag's colon must stay ASCII :, not a fullwidth . Get it wrong and the line silently falls back to plain prose — no warning, unlike the mid-line case above.

Writing also feeds discoverability. Kozou's search_schema searches not just object names but COMMENT bodies and @ai / @policy notes too. The words you put in a note become the handle an agent uses to find the meaning.

The code fragments below are the real thing from Kozou's quickstart demo schema. Inside a COMMENT string, '' (two single quotes) is an escaped single quote; the @ai: / @policy: notes are kept one per line (per the rule), while the CREATE VIEW SELECT further down is reflowed for readability.

@ai: — name the trap, point to the source of truth

@ai is free text for the agent. The trick to making it land is to go past "what this column is" and write what the agent should and shouldn't do — usually in the imperative. Three from the demo schema.

First, the clearest trap: a column you must not use.

COMMENT ON COLUMN orders.amount_total IS 'DEPRECATED denormalized order total.
@ai: Do NOT use this for reporting — it is a stale cache the application stopped maintaining, can disagree with the line items, and includes test orders. Compute revenue from vw_recognized_revenue instead.';
Enter fullscreen mode Exit fullscreen mode

amount_total has a plausible name ("total") and a numeric(12,2) type. Asked for revenue, an agent naturally reaches for SUM. But it's a cache the application stopped maintaining, out of step with the line items, with test orders mixed in. The pattern here: don't stop at "don't use it" — always add the way out, "use vw_recognized_revenue instead." Naming a trap without an exit leaves the agent with nowhere to go next.

Next, an exclusion that always applies.

COMMENT ON COLUMN orders.is_test IS 'Internal QA / load-test order flag.
@ai: ALWAYS exclude is_test = true from revenue, order counts, and dashboards — these are not real customer orders.';
Enter fullscreen mode Exit fullscreen mode

The pattern: use strong words (ALWAYS / NEVER) and spell out the scope. Saying "exclude it from revenue, from counts, and from dashboards" keeps the agent from misjudging which contexts the rule covers.

Third, the nastiest kind — the one that fails silently.

COMMENT ON COLUMN products.list_price IS 'The CURRENT catalog price.
@ai: This price changes over time. NEVER use it to value past orders — each order captured its own price in order_items.unit_price; joining list_price onto historical orders silently misprices revenue.';
Enter fullscreen mode Exit fullscreen mode

Value a past order with list_price (the current catalog price) and the JOIN succeeds, the query throws no error, and only the number is quietly wrong. The price to use is the one captured at order time, order_items.unit_price. For traps that produce no error, just a wrong result, write down why it's wrong — otherwise no one has a thread to pull to catch it.

So how does an @ai you wrote reach the agent? When Kozou reads the schema, an @ai line comes back over MCP (the standard protocol for connecting AI agents to external tools) as aiDescription, sitting right next to the column.

{
  "name": "amount_total",
  "dataType": "numeric(12,2)",
  "aiDescription": "Do NOT use this for reporting — it is a stale cache the application stopped maintaining, can disagree with the line items, and includes test orders. Compute revenue from vw_recognized_revenue instead."
}
Enter fullscreen mode Exit fullscreen mode

The note you wrote arrives beside the column, in the words you wrote it in. Stack several @ai: lines on one column and they're joined with newlines into a single aiDescription.

@policy: — advisory, not enforcement

If @ai is "here's how I'd like you to behave," @policy is "record this as a rule that must not be broken." Two from the demo schema.

COMMENT ON TABLE orders IS 'Customer orders.
@ai: An order is recognized revenue only when status = ''paid'' AND is_test = false AND deleted_at IS NULL and its customer is not soft-deleted; value each line at order_items.unit_price (the captured price), not products.list_price.
@ai: The vw_recognized_revenue view already applies every one of these rules — start there for any revenue question.
@policy: ''refunded'' and ''chargeback'' reverse a sale; never count them as revenue.';
Enter fullscreen mode Exit fullscreen mode
COMMENT ON TABLE customers IS 'People who place orders.
@ai: Rows with deleted_at IS NOT NULL are soft-deleted (kept for audit / legal retention); exclude them from customer-facing queries, counts, and metrics.
@policy: Treat email as personal data; do not expose it in aggregate or public reports.';
Enter fullscreen mode Exit fullscreen mode

There's a line here, the same one the introduction drew. @policy is advisory for the agent, not enforcement. Writing "don't expose email in aggregate or public reports" in a @policy does not, by itself, stop access. The actual enforcement stays on the PostgreSQL side — privileges (GRANT) and row-level security (RLS). Keep supplying meaning separate from enforcing permissions — it's Kozou's design stance, and a line worth holding as an author too. If you truly must hide something, stop it with a privilege, not a comment.

@policy arrives separately from @ai (a single newline-joined string) — as an array of rules. "How I'd like you to think" (@ai) and "a rule you must not break" (@policy) reach the agent as different things. A rule of thumb: "I want the agent to think this way" → @ai; "I want this recorded as a rule" → @policy. But never forget that what enforces a rule is a DB privilege, not a comment.

Make a view a named concept

So far we've been adding notes to columns and tables. There's a level above that: define the right way of doing something as a view.

If "the correct way to compute recognized revenue" lives in someone's head, turning it into a view makes it a named, executable concept. The demo's vw_recognized_revenue is exactly that.

CREATE VIEW vw_recognized_revenue AS
  SELECT o.id AS order_id, o.customer_id, o.placed_at, o.channel,
         sum(oi.quantity * oi.unit_price - oi.discount) AS net_revenue
  FROM orders o
  JOIN customers c ON c.id = o.customer_id AND c.deleted_at IS NULL
  JOIN order_items oi ON oi.order_id = o.id
  WHERE o.status = 'paid' AND o.is_test = false AND o.deleted_at IS NULL
  GROUP BY o.id, o.customer_id, o.placed_at, o.channel;
Enter fullscreen mode Exit fullscreen mode

Excluding test orders, excluding soft-deleted orders and customers, counting only 'paid', valuing each line at the captured unit price — all baked into the definition. Rules that were scattered collapse into one executable definition.

Give that view a COMMENT ON too.

COMMENT ON VIEW vw_recognized_revenue IS 'Authoritative recognized revenue, one row per paid order.
@ai: This is the source of truth for revenue — it already excludes test orders, soft-deleted orders and customers, and non-paid statuses, and values each line at the captured unit_price minus discount. Start here for any revenue / sales question; do not re-derive from orders.amount_total or products.list_price.
@example: Revenue by quarter.
  SELECT date_trunc(''quarter'', placed_at) AS quarter,
         sum(net_revenue) AS revenue
  FROM vw_recognized_revenue
  GROUP BY 1
  ORDER BY 1;';
Enter fullscreen mode Exit fullscreen mode

The pattern: in the view's @ai, declare it the source of truth — start here, don't re-derive from the raw tables — and attach a representative use in @example. An @example is a description line followed by indented SQL; it's the one note allowed to span multiple lines. That's why, above, @ai is one line and @example is several.

Concepts stack. Another view can build on this one.

COMMENT ON VIEW vw_customer_lifetime_value IS 'Total recognized revenue per active customer.
@ai: Lifetime value reuses vw_recognized_revenue, so the same recognition rules apply automatically; soft-deleted customers are excluded.';
Enter fullscreen mode Exit fullscreen mode

vw_customer_lifetime_value selects from vw_recognized_revenue, so its rules — excluding test orders and soft deletes — apply automatically; there's no filter to rewrite. What's inherited is the executable definition, not the notes: each view still carries its own @ai, and this one's says, in as many words, that it reuses vw_recognized_revenue. Stack a concept on a concept and the rules you wrote once keep working from underneath — but you still write a note on each concept.

Kozou treats such views as domain concepts. Call get_concept_context on one and the agent gets back the view's @ai notes, a recommended query source, related tables, and the @example queries — together. Instead of reassembling the business rules from raw tables, the agent heads for the view that already encapsulates them.

And when you cross tables: the ON condition itself, a machine can derive from the foreign key. But why this JOIN exists is the meaning a human writes on the FK constraint. Put a COMMENT on the FK constraint and Kozou hands its text over as the JOIN's purpose, alongside the ON. The demo schema doesn't comment its foreign keys, so here's an illustrative example of how to write one.

-- Illustrative (not in the demo schema)
COMMENT ON CONSTRAINT orders_customer_id_fkey ON orders IS 'Each order belongs to exactly one customer.
@ai: Join through this FK to attribute revenue to a customer; do not match on email or name.';
Enter fullscreen mode Exit fullscreen mode

Write "what this relationship is for" plus "how to join it (avoid loose matches)," and you can point an agent — one that might otherwise join on email or name despite the foreign key — at the right way to connect the tables.

Notes go stale — keep them maintained

Written meaning has a shelf life. @ai and @policy are primary assets, but change the schema without updating the notes and the agent will confidently believe a claim that's gone stale. A column you marked "don't use" that has quietly come back into service is a mismatch harder to catch than a raw-DDL one — because the note states the wrong thing with full confidence.

So when a migration changes a column or a rule, fix the matching @ai / @policy in the same change. There's generally no test guarding COMMENT freshness, so the last line of defense is review by eye. When you read a schema diff, get in the habit of reading the comment diff alongside it.

The meaning you wrote reaches the agent as-is

Writing the meaning is itself something you do on plain PostgreSQL. COMMENT ON and view definitions live in the same place as the schema and migrate alongside it — primary assets. The habit pays off even without Kozou: onboarding a new hire, aligning understanding in review, carrying context across a future tool switch. Meaning bundled into the schema means anyone reading it hits the same primary source.

Delivering that written meaning to the agent — without summarizing or paraphrasing — is what Kozou does, over MCP; the introduction covers that side. The one thing worth restating here, because it matters when you're the author: Kozou's MCP is describe-only by default — there's no tool for generating, executing, or writing SQL out of the box. Supplying meaning and operating the database stay clearly apart, so writing rich notes doesn't widen what an agent can do to your data.

Setup, per-client configuration, and the real (English) demo schema are all at kozou.org and on GitHub. I won't restate the steps here — how to do it is what the docs are the source of truth for. (Deriving an Admin UI, REST, docs, and types from the same schema-plus-comments — "one definition, many faithful forms" — is a post of its own.)

In summary

  • Meaning goes not in column names or types but in COMMENT ON and view definitions. The craft: start-of-line tags, one @ai/@policy per line, stack lines for more points — and the body can be in any language (only the tag name and its : are ASCII).
  • @ai names the trap and points to the source of truth ("don't use it" + "use the authoritative view instead"). @policy records a rule that must not be broken — but it's advisory; enforcement stays with DB privileges and RLS.
  • A view can be a named concept. Give the authoritative view an @ai (declaring it the source of truth) and an @example (a representative query); build one view on another and the rules are inherited — though each view still needs its own note. An FK's comment travels as the JOIN's purpose.
  • Notes aren't write-and-forget. Change the schema and update the notes in the same change — a stale note misleads the agent with full confidence.
  • Then Kozou delivers what you wrote to the agent, as-is. Write the meaning into the DB first.

The repository is github.com/kozou-dev/kozou (Apache-2.0); the docs are at kozou.org. The demo schema ships with the quickstart. Try adding one @ai: line to your own schema and start there.

The Japanese version of this post is already live.

Top comments (0)