Annotations
Ontogen reads #[ontology(...)] attributes from your schema structs at build time using syn. The #[derive(OntologyEntity)] macro itself is a no-op — it just makes the attributes legal Rust. All the real work happens in build.rs when parse_schema() processes the source files.
Struct-level annotations
Section titled “Struct-level annotations”Applied to the struct itself, alongside #[derive(OntologyEntity)].
#[derive(Debug, Clone, Serialize, Deserialize, OntologyEntity)]#[ontology(entity, table = "workouts", directory = "workouts", type_name = "workout", prefix = "wkt")]pub struct Workout { // ...}| Attribute | Required | Default | Description |
|---|---|---|---|
entity | Yes | — | Marks this struct as an Ontogen entity. Required for the struct to be picked up by parse_schema(). |
table | No | snake_case of struct name | SeaORM table name (e.g., "workouts"). |
directory | No | snake_case of struct name | Subdirectory for Markdown files (e.g., "workouts"). Used by gen_markdown_io(). |
type_name | No | the struct name (e.g., "Workout") | The type: value the markdown backend writes as the first frontmatter key of every record. Overrides the default (e.g., "workout"). |
prefix | No | snake_case of struct name | ID prefix for global uniqueness (e.g., "wkt"). Useful when IDs are prefixed like wkt-abc123. |
id | No | the store’s IdStrategy | How the store fills the id of a record of this entity created without one: "provided", "uuid" or "slug(<field>)". See Id strategy. |
Minimal form
Section titled “Minimal form”Most of the time you don’t need all the attributes. If your struct name matches your conventions, this is sufficient:
#[derive(OntologyEntity)]#[ontology(entity, table = "exercises")]pub struct Exercise { // ...}When directory and prefix are omitted they default to the snake_case version of the struct name (e.g., "exercise" for Exercise). type_name defaults to the struct name as written ("Exercise").
The snake_case name also names the entity’s generated modules. When it is a Rust keyword (Match gives match) the modules are raw identifiers (r#match), while files, tables and directories keep the bare name.
A few entity names would collide with what the generated code names bare, so the build refuses them with an error that names the entity, says what it would clash with and suggests a rename:
CrateandSuper, whose snake_case namescrateandsuperhave no raw form.- The prelude items the generated code uses:
Result,Option,Vec,String,Box,From,Default,Clone,Send,SyncandFn. - Names starting with
Ontogen(OntogenWidget, notOntogeny), reserved for the types ontogen generates. - Names whose module would shadow a crate that the
JsonSchemaderive names:StdandSchemars. - With a store:
Store,AppError,ChangeOp,EntityKindandOrderBy. With API forwarders over the store:Hooks,Generated, and a name whose module is the API output directory’s. - With servers: the state and store types (
AppState,Store); with an MCP server,McpToolDef,SimpleToolDef,GetByIdInput,ByIntIdInputandEmptyInput; with a paginated IPC server,PaginatedResult. The servers refuse these names, and theOntogenprefix, for any type your API functions name, not only entities.
A field may not start with ontogen_, the prefix of the generated code’s own bindings.
Id strategy
Section titled “Id strategy”id = "..." overrides the store-wide IdStrategy (Pipeline::store_id_strategy) for one entity, on either backend:
#[derive(OntologyEntity)]#[ontology(entity, directory = "events", id = "uuid")]pub struct Event { #[ontology(id)] pub id: String, pub title: String,}| Value | Strategy | A create without an id |
|---|---|---|
"provided" | IdStrategy::Provided | fails with {Entity}IdRequired |
"uuid" | IdStrategy::Uuid | gets a fresh UUID v4 |
"slug(<field>)" | IdStrategy::SlugFromField("<field>") | gets the slug of <field>, probing -2, -3, … |
The value must be one of these strings, spelled exactly (lowercase, no spaces), and slug(<field>) must name a plain String field of the same entity (not Option<String>). Anything else fails the build with a CodegenError that names the entity and lists the accepted forms. An entity without id uses the store’s default, which is required even when every entity sets its own.
Field-level annotations
Section titled “Field-level annotations”Applied to individual fields within an entity struct.
#[ontology(id)]
Section titled “#[ontology(id)]”Marks the primary key field. Every entity should have exactly one id field. The field is typically String.
#[ontology(id)]pub id: String,Effect on generated code:
- SeaORM entity gets
#[sea_orm(primary_key, auto_increment = false)]. - Store CRUD:
get_*()looks up by this field;delete_*()deletes by this field. - DTOs: the field is included in
CreateInputbut excluded fromUpdateInput(you can’t change the ID).
#[ontology(body)]
Section titled “#[ontology(body)]”Marks a field as the Markdown body content, not part of YAML frontmatter.
#[ontology(body)]pub content: String,Effect on generated code:
- Markdown writer puts this field’s value below the
---frontmatter separator. - Markdown parser reads everything after frontmatter into this field.
- SeaORM entity includes it as a regular column.
#[ontology(enum_field)]
Section titled “#[ontology(enum_field)]”Marks a field whose type is a Rust enum, stored as a string in the database.
#[ontology(enum_field)]pub status: Option<TaskStatus>,Effect on generated code:
- SeaORM column type is
String(orOption<String>). - Conversions use
.to_string()/.parse()to go between the enum and its string representation. - DTOs use the string form.
#[ontology(frontmatter_name = "...")]
Section titled “#[ontology(frontmatter_name = "...")]”Renames the field’s key in markdown frontmatter. Nothing else changes: the Rust field, DTOs, API, servers, clients and SeaORM columns keep the Rust name.
#[ontology(frontmatter_name = "task_status")]pub status: String,Use it to step around the keys OKF reserves. The key rules and build errors are in reserved frontmatter keys.
#[ontology(skip)]
Section titled “#[ontology(skip)]”Excludes a field from all code generation. The field exists in your Rust struct but Ontogen pretends it’s not there.
#[ontology(skip)]pub cached_value: Option<String>,Effect on generated code:
- Not included in SeaORM entity.
- Not included in DTOs.
- Not included in Markdown frontmatter.
- Not included in from_model/to_active_model conversions.
#[ontology(multiline_list)]
Section titled “#[ontology(multiline_list)]”Rendering hint for Vec<String> fields in Markdown output. Write each item on its own line (YAML block sequence) instead of inline [a, b, c].
#[ontology(multiline_list)]pub tags: Vec<String>,Markdown output with multiline_list:
tags: - "[[tag-1]]" - "[[tag-2]]"Markdown output without (default):
tags: ["[[tag-1]]", "[[tag-2]]"]#[ontology(default_value = "...")]
Section titled “#[ontology(default_value = "...")]”Rendering hint for Markdown output. When the field’s value equals this default, skip rendering it in frontmatter entirely.
#[ontology(default_value = "active")]pub status: String,If status is "active", the field won’t appear in the Markdown frontmatter. This keeps Markdown files clean when most entities have the default value.
Relation annotations
Section titled “Relation annotations”Relations are field-level annotations that describe cross-entity references. They affect SeaORM entity generation (relation enums, junction tables), store generation (population helpers, junction sync), and Markdown I/O (wikilink formatting).
belongs_to
Section titled “belongs_to”A many-to-one relationship. This entity has a foreign key column pointing to the target.
#[ontology(relation(belongs_to, target = "Workout"))]pub workout_id: String,| Attribute | Required | Description |
|---|---|---|
target | Yes | The target entity name (e.g., "Workout"). Must match a struct name in your schema. |
Field type: String for a required FK, Option<String> for an optional FK.
Effect on generated code:
- SeaORM:
Relationenum gets aBelongsTovariant withfrom/tocolumn mapping. - Store: no special handling beyond the normal column.
- Markdown: the field value is formatted as a wikilink (e.g.,
"[[wkt-abc123]]").
has_many
Section titled “has_many”A one-to-many reverse relationship. The target entity has a FK column pointing back to this entity. No additional column is needed on this entity. The target must be the declaring entity itself; see has_many writes.
#[ontology(relation(has_many, target = "Node", foreign_key = "parent_id"))]pub contains: Vec<String>,| Attribute | Required | Description |
|---|---|---|
target | Yes | The target entity name. Must be the declaring entity. |
foreign_key | Yes | The FK column on the target entity that references this entity’s ID. A keyword field can be named bare or raw: "in" and "r#in" both name r#in. |
Field type: Vec<String>. Populated at read time by querying the target table.
Effect on generated code:
- SeaORM:
Relationenum gets aHasManyvariant. - Store:
populate_*_relations()loads the related IDs after fetching the parent. - DTOs: the field appears in inputs, accepting a list of related IDs.
many_to_many
Section titled “many_to_many”A many-to-many relationship through a junction table.
#[ontology(relation(many_to_many, target = "Tag"))]pub tags: Vec<String>,With optional explicit junction table name:
#[ontology(relation(many_to_many, target = "Tag", junction = "workout_tags"))]pub tags: Vec<String>,| Attribute | Required | Description |
|---|---|---|
target | Yes | The target entity name. |
junction | No | Explicit junction table name. When omitted, derived as {source_table}_{field_name} (e.g., workout_tags). |
Field type: Vec<String>. Populated at read time from the junction table.
Effect on generated code:
- SeaORM: a full junction table entity is generated (e.g.,
workout_tags) withBelongsTorelations to both sides. The source entity getsRelated<TargetEntity>withvia(). - Store:
create_*()andupdate_*()callsync_junction()to keep the junction table in sync.populate_*_relations()loads related IDs via the junction table. - DTOs: the field appears in both
CreateInputandUpdateInputasVec<String>.
Combination rules
Section titled “Combination rules”Here’s what’s valid and what’s not:
| Annotation | Valid field types | Notes |
|---|---|---|
id | String | Exactly one per entity. |
body | String | At most one per entity. Only meaningful for Markdown I/O. |
enum_field | Option<EnumType>, EnumType | The enum must handle its own string conversion. |
skip | Any | Excluded from everything. |
multiline_list | Vec<String>, Vec<T> | Markdown rendering only. |
default_value | String, Option<String> | Markdown rendering only. |
relation(belongs_to) | String, Option<String> | FK column on this entity. |
relation(has_many) | Vec<String> | Reverse FK on the target, which must be the declaring entity. Requires foreign_key. |
relation(many_to_many) | Vec<String> | Junction table. Optional junction attribute. |
Fields with no #[ontology(...)] annotation are treated as FieldRole::Plain. They’re included in all generated code as regular data fields.
Required derives
Section titled “Required derives”Your entity struct needs these derives alongside OntologyEntity:
#[derive(Debug, Clone, Serialize, Deserialize, OntologyEntity)]SerializeandDeserializeare needed because generated DTOs, transport layers, and Markdown I/O all depend on serde.Cloneis needed because several generated functions clone entities (e.g., cloning the entity before mutation in update flows).Debugis not strictly required but strongly recommended — error messages reference entity state.