How to extend Sandbar's metamodel with new domain classes —
:dt/Propertydeclarations,:dt/Classdefinitions, slot composition, validation, and registration with:required-schema. For the worked example, seezorp-tutorial.md; for the theoretical background seedoc/concepts/metamodel.md.
Add a class when:
:order/Order, :inventory/Item, :event/Booking).Do not add a class for one-off computed values, ephemeral per-request shapes, or anything that maps better to an attribute on an existing class.
A class is an entity satisfying these constraints:
{:db/ident :your-ns/YourClass
:dt/type :dt/Class
:dt/subclass-of :dt/Resource ; or any other :dt/Class
:dt/abstract? false ; optional — true forbids direct instantiation
:dt/slots [:your-ns/slot1 :your-ns/slot2 ...]
:dt/context "your-ns" ; namespace label
:dt/label "Human-Readable Name"
:db/doc "Short description of the class."}
A property is also an entity:
{:db/ident :your-ns/slot1
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:dt/type :dt/Property
:dt/domain :your-ns/YourClass
:dt/range :db.type/string
:dt/required? true ; optional — required slots must be present on instances
:db/doc "Documentation for the slot."}
:event/Booking classA worked example: model a calendar booking with required fields and validation.
:event/Booking
├── :event.booking/title (string, required)
├── :event.booking/starts-at (instant, required)
├── :event.booking/ends-at (instant, required)
├── :event.booking/owner (ref → :model/User, required)
├── :event.booking/location (string, optional)
└── :event.booking/description (string, optional)
Create schema/event.edn:
[
;; Forward declaration (avoids order-of-load issues)
[{:db/id #db/id[:db.part/user -1]
:db/ident :event/Booking}]
;; Properties
[{:db/ident :event.booking/title
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:dt/type :dt/Property
:dt/domain :event/Booking
:dt/range :db.type/string
:dt/required? true
:db/doc "Short title for the booking"
:db.install/_attribute :db.part/db}
{:db/ident :event.booking/starts-at
:db/valueType :db.type/instant
:db/cardinality :db.cardinality/one
:dt/type :dt/Property
:dt/domain :event/Booking
:dt/range :db.type/instant
:dt/required? true
:db/doc "Booking start time (UTC)"
:db.install/_attribute :db.part/db}
{:db/ident :event.booking/ends-at
:db/valueType :db.type/instant
:db/cardinality :db.cardinality/one
:dt/type :dt/Property
:dt/domain :event/Booking
:dt/range :db.type/instant
:dt/required? true
:db/doc "Booking end time (UTC)"
:db.install/_attribute :db.part/db}
{:db/ident :event.booking/owner
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one
:dt/type :dt/Property
:dt/domain :event/Booking
:dt/range :model/User
:dt/required? true
:db/doc "The user who owns this booking"
:db.install/_attribute :db.part/db}
{:db/ident :event.booking/location
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:dt/type :dt/Property
:dt/domain :event/Booking
:dt/range :db.type/string
:db/doc "Optional location string"
:db.install/_attribute :db.part/db}
{:db/ident :event.booking/description
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:dt/type :dt/Property
:dt/domain :event/Booking
:dt/range :db.type/string
:db/doc "Optional free-form description"
:db.install/_attribute :db.part/db}]
;; Class definition (after properties so :dt/slots can reference them)
[{:db/ident :event/Booking
:dt/type :dt/Class
:dt/subclass-of :dt/Resource
:dt/context "event"
:dt/label "Booking"
:db/doc "A calendar booking owned by a user"
:dt/slots [:event.booking/title
:event.booking/starts-at
:event.booking/ends-at
:event.booking/owner
:event.booking/location
:event.booking/description]}]
]
In config/config.edn, add :event to :required-schema:
{:datomic-uri "datomic:dev://localhost:4334/sandbar"
:http-port 8080
:required-schema [:meta :literal :ref :fn :any :workflow :mm :user :event]}
In the REPL:
(stop)
(go)
The schema loads as part of startup. Verify:
(dt/all-classes)
;; => (... :event/Booking ...)
(dt/slots-of :event/Booking)
;; => #{:event.booking/title :event.booking/starts-at ...}
(dt/required-slots-of :event/Booking)
;; => #{:event.booking/title :event.booking/starts-at
;; :event.booking/ends-at :event.booking/owner}
(dt/make :event/Booking
{:event.booking/title "Weekly Sync"
:event.booking/starts-at #inst "2026-05-14T15:00:00.000Z"
:event.booking/ends-at #inst "2026-05-14T16:00:00.000Z"
:event.booking/owner (dt/find-by :user/login "alice")
:event.booking/location "Conference Room B"})
;; => entity; validation passed; transacted
Try omitting a required slot:
(dt/make :event/Booking
{:event.booking/title "Weekly Sync"})
;; throws — :missing-required for starts-at, ends-at, owner
The class is immediately surfaced through every projection:
;; Clojure
(dt/all-instances-of :event/Booking)
;; REST
;; GET /api/store/classes/event/Booking/instances
;; MCP
;; tools/call sandbar.class.instances {class: "event/Booking"}
No code change, no restart, no registration table. The class is in the metamodel; the surface reflects it.
A new class that subtypes an existing class declares it via :dt/subclass-of:
{:db/ident :event/RecurringBooking
:dt/type :dt/Class
:dt/subclass-of :event/Booking
:dt/context "event"
:dt/label "RecurringBooking"
:dt/slots [:event.recurrence/rule]}
Slot inheritance is transitive: :event/RecurringBooking automatically inherits every slot from :event/Booking (and every ancestor up to :dt/Resource).
Mark a class abstract when it exists to share slots/structure but should never be directly instantiated:
{:db/ident :event/AbstractBooking
:dt/type :dt/Class
:dt/subclass-of :dt/Resource
:dt/abstract? true
:dt/slots [...]}
dt/make :event/AbstractBooking {...} throws :abstract-class.
For class-specific validation logic that the slot system doesn't express:
;; In your Clojure code
(defn validate-booking [entity]
(when (and (:event.booking/starts-at entity)
(:event.booking/ends-at entity)
(.before (:event.booking/ends-at entity)
(:event.booking/starts-at entity)))
{:slot :event.booking/ends-at
:error :ends-before-starts
:message "Booking end must be after start"}))
;; In schema
{:db/ident :event/Booking
:dt/type :dt/Class
:dt/validator 'my.namespace/validate-booking
...}
The validator is a fully-qualified symbol; dt/validate-data invokes it after structural checks.
If your class produces hierarchical content (sections, sub-resources), consider path-derived idents as :mm/Section uses. See doc/concepts/markdown-as-canonical.md for the discipline.
If your class has a canonical wire format (markdown? RDF? custom IDL?), bind a codec via :dt/native-codec:
{:db/ident :event/Booking
:dt/type :dt/Class
:dt/native-codec :codec/json
...}
Then sandbar.entity.create :event/Booking {format: "json", source: "..."} routes through sandbar.codec.json. Authoring a new codec is covered in implementing-a-codec.md.
Forgetting :db.install/_attribute :db.part/db. Properties need this transaction-time directive to install as Datomic attributes; missing it produces silent non-installation.
Forward-declaring without backfilling. If you declare an ident first and then reference it in :dt/domain / :dt/range of another property, that's fine — but you must transact the full class definition in a later transaction. Half-declared classes pass schema loading but fail validation.
Skipping :dt/context and :dt/label. These power presentational projections (REST descriptions, MCP titles). They're optional but most consumers expect them.
Required ref-typed slots without resolution. If :event.booking/owner requires a :model/User, ensure your dt/make call passes a fully-resolved user entity (looked up by dt/find-by) rather than a bare ident or string login.
Cardinality-many slots stored as scalars. If a slot is :db.cardinality/many, the value must be a vector or set, even with one element: {:event.booking/tags ["work"]}, not {:event.booking/tags "work"}.
zorp-tutorial.md — a full worked example with inheritancedoc/concepts/metamodel.md — the theoretical foundationsimplementing-a-codec.md — binding a wire format to a new classdesigning-workflows.md — when your class's lifecycle is workflow-shapeddoc/api/dt-star.md — every dt/* function for introspection + creationCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |