What do the annotations do for the agent?¶
An annotation is not decoration. Every attribute you author lands somewhere specific and does a specific job for the model reading it.
This page is the chain: what you write → what gets built → what the agent sees.
The two authoring surfaces¶
semantido deliberately has only two.
The decorator — table-level meaning¶
@semantic_table(
description="Customer orders — one row per order.",
synonyms=["orders", "purchases"],
business_context="total_amount is gross, including tax and shipping.",
application_context="Checkout and fulfilment.",
sql_filters=["orders.tenant_id = :tenant_id"],
time_dimension="ordered_at",
)
class Order(SemanticDeclarativeBase):
...
Each argument answers a question the schema can't:
| Argument | The question it answers |
|---|---|
description |
What is a row of this table? |
synonyms |
What might a user call this thing? |
business_context |
What must someone know before trusting a number from here? |
application_context |
Which part of the system owns this? |
sql_filters |
What should always apply — tenancy, soft deletes? |
time_dimension |
When someone says "last month", which column do they mean? |
The decorator writes these to dunders (__semantic_description__, and so on); you can set those directly instead. time_dimension and __semantic_time_dimension__ conflicting on the same class body raises ValueError rather than silently picking one.
The column convention — field-level meaning¶
There is no column decorator. semantido reads class attributes named <column>_<concern>:
class Order(SemanticDeclarativeBase):
total_amount = Column(Numeric(12, 2))
total_amount_description = "Gross order total, including tax and shipping."
total_amount_synonyms = ["order value", "revenue"]
total_amount_application_rules = ["Do not SUM across order_lines — it fans out."]
status_sample_values = ["PENDING", "SHIPPED", "CANCELLED"]
email_privacy_level = PrivacyLevel.CONFIDENTIAL
ordered_at_time_grain = TimeGrain.SECOND
This looks unusual and is a deliberate trade — it costs IDE autocomplete on the metadata, and buys the ability to annotate any mapped column, including from mixins, inherited bases, or __table__ reflection. See Architecture.
Full list: semantic metadata reference.
What you never author¶
Everything the mapper already knows is extracted:
- primary keys
- foreign keys and their
table.columntargets - relationships, their join conditions, and their cardinality
- column data types, normalised
This is the core economy of the design. You author what the schema cannot express, and nothing else. A join condition typed by hand is a join condition that can be wrong.
What gets built¶
sync_semantic_layer() walks the registry and produces a SemanticLayer — a plain dataclass tree:
SemanticLayer
├── tables: dict[str, Table]
│ └── Table(name, description, columns, primary_key, schema, synonyms,
│ sql_filters, application_context, business_context, time_dimension)
│ └── columns: list[Column]
│ └── Column(name, data_type, description, privacy_level, sample_values,
│ synonyms, is_foreign_key, references, application_rules,
│ is_time_dimension, time_grain)
├── relationships: list[Relationship]
│ └── Relationship(from_table, to_table, join_condition, relationship_type, description)
└── application_glossary: dict[str, str]
No database connection, no I/O. Build it, assert on it in tests, mutate it before export.
What the agent sees¶
The SemanticLayer is not what reaches the model — an exporter is, and the choice matters:
to_markdown— terse, prose-shaped, for the system prompt.to_json— the tree, empty values pruned, for your own code.to_osi_yaml— interchange, when something else is the consumer.
to_markdown renders roughly this:
### orders
- **Primary Key**: order_id
- **Description**: Customer orders — one row per order.
- **Synonyms**: orders, purchases
- **Business Context**: total_amount is gross, including tax and shipping.
#### Columns
- **total_amount** (DECIMAL)
- Gross order total, including tax and shipping.
- *Synonyms*: order value, revenue
Note what's absent: no CREATE TABLE, no nullability, no indexes. That absence is the product — see Correctness.
In short¶
- The decorator carries table meaning; the
<column>_*convention carries field meaning. - The mapper carries structure, and you should never retype it.
- The
SemanticLayeris the IR — inspectable, testable, plain. - The exporter decides what the model reads, and the Markdown one is deliberately thin.