Skip to content

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.

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 {
// ...
}
AttributeRequiredDefaultDescription
entityYes—Marks this struct as an Ontogen entity. Required for the struct to be picked up by parse_schema().
tableNosnake_case of struct nameSeaORM table name (e.g., "workouts").
directoryNosnake_case of struct nameSubdirectory for Markdown files (e.g., "workouts"). Used by gen_markdown_io().
type_nameNothe 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").
prefixNosnake_case of struct nameID prefix for global uniqueness (e.g., "wkt"). Useful when IDs are prefixed like wkt-abc123.
idNothe store’s IdStrategyHow the store fills the id of a record of this entity created without one: "provided", "uuid" or "slug(<field>)". See Id strategy.

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:

  • Crate and Super, whose snake_case names crate and super have no raw form.
  • The prelude items the generated code uses: Result, Option, Vec, String, Box, From, Default, Clone, Send, Sync and Fn.
  • Names starting with Ontogen (OntogenWidget, not Ontogeny), reserved for the types ontogen generates.
  • Names whose module would shadow a crate that the JsonSchema derive names: Std and Schemars.
  • With a store: Store, AppError, ChangeOp, EntityKind and OrderBy. 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, ByIntIdInput and EmptyInput; with a paginated IPC server, PaginatedResult. The servers refuse these names, and the Ontogen prefix, 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 = "..." 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,
}
ValueStrategyA create without an id
"provided"IdStrategy::Providedfails with {Entity}IdRequired
"uuid"IdStrategy::Uuidgets 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.


Applied to individual fields within an entity struct.

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 CreateInput but excluded from UpdateInput (you can’t change the ID).

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.

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 (or Option<String>).
  • Conversions use .to_string() / .parse() to go between the enum and its string representation.
  • DTOs use the string form.

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.

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.

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]]"]

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.


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).

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,
AttributeRequiredDescription
targetYesThe 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: Relation enum gets a BelongsTo variant with from/to column mapping.
  • Store: no special handling beyond the normal column.
  • Markdown: the field value is formatted as a wikilink (e.g., "[[wkt-abc123]]").

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>,
AttributeRequiredDescription
targetYesThe target entity name. Must be the declaring entity.
foreign_keyYesThe 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: Relation enum gets a HasMany variant.
  • Store: populate_*_relations() loads the related IDs after fetching the parent.
  • DTOs: the field appears in inputs, accepting a list of related IDs.

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>,
AttributeRequiredDescription
targetYesThe target entity name.
junctionNoExplicit 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) with BelongsTo relations to both sides. The source entity gets Related<TargetEntity> with via().
  • Store: create_*() and update_*() call sync_junction() to keep the junction table in sync. populate_*_relations() loads related IDs via the junction table.
  • DTOs: the field appears in both CreateInput and UpdateInput as Vec<String>.

Here’s what’s valid and what’s not:

AnnotationValid field typesNotes
idStringExactly one per entity.
bodyStringAt most one per entity. Only meaningful for Markdown I/O.
enum_fieldOption<EnumType>, EnumTypeThe enum must handle its own string conversion.
skipAnyExcluded from everything.
multiline_listVec<String>, Vec<T>Markdown rendering only.
default_valueString, 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.


Your entity struct needs these derives alongside OntologyEntity:

#[derive(Debug, Clone, Serialize, Deserialize, OntologyEntity)]
  • Serialize and Deserialize are needed because generated DTOs, transport layers, and Markdown I/O all depend on serde.
  • Clone is needed because several generated functions clone entities (e.g., cloning the entity before mutation in update flows).
  • Debug is not strictly required but strongly recommended — error messages reference entity state.