Skip to content
cccskills
FreePublished on GitHub

grain-service

Create a complete Grain domain service with schemas, read models, commands, queries, and tests

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md7.3 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Create a Grain Domain Service (ORC Library)

This skill walks through creating a domain service component in the ORC library. No UI, no web layer — services expose data through commands, queries, and read models.

Naming conventions:

  • Service name: widgets (kebab-case for ns, used as event/command prefix)
  • Entity name: widget (singular)
  • Component dir: components/widget-service/

Step 1: Create Component Structure

components/widget-service/
  deps.edn
  src/ai/obney/orc/widget_service/
    interface.clj
    interface/schemas.clj
    core/
      read_models.clj
      commands.clj
      queries.clj
      todo_processors.clj
  test/ai/obney/orc/widget_service/
    test_helpers.clj
    commands_test.clj

deps.edn:

{:paths ["src" "resources"]
 :deps {}
 :aliases {:test {:extra-paths ["test"]
                  :extra-deps {}}}}

Step 2: Define Schemas (interface/schemas.clj)

(ns ai.obney.orc.widget-service.interface.schemas
  (:require [ai.obney.grain.schema-util.interface :refer [defschemas]]))

(defschemas events
  {:widgets/widget-created [:map [:widget-id :uuid] [:name :string] [:description [:maybe :string]]]
   :widgets/widget-updated [:map [:widget-id :uuid] [:name :string] [:description [:maybe :string]]]
   :widgets/widget-deleted [:map [:widget-id :uuid]]})

(defschemas commands
  {:widgets/create-widget [:map
                           [:name [:string {:min 1 :error/message "Name is required"}]]
                           [:description {:optional true} [:maybe :string]]]
   :widgets/update-widget [:map
                           [:widget-id :uuid]
                           [:name [:string {:min 1 :error/message "Name is required"}]]
                           [:description {:optional true} [:maybe :string]]]
   :widgets/delete-widget [:map
                           [:widget-id :uuid]]})

(defschemas queries
  {:widgets/get-all [:map]
   :widgets/get-by-id [:map [:widget-id :uuid]]})

Step 3: Build Read Model (core/read_models.clj)

(ns ai.obney.orc.widget-service.core.read-models
  (:require [ai.obney.grain.read-model-processor-v2.interface :as rmp :refer [defreadmodel]]))

(def widget-event-types
  #{:widgets/widget-created
    :widgets/widget-updated
    :widgets/widget-deleted})

(defmulti widgets* (fn [_state event] (:event/type event)))

(defmethod widgets* :widgets/widget-created
  [state {:keys [widget-id name description]}]
  (assoc state widget-id {:widget-id widget-id :name name :description description :active true}))

(defmethod widgets* :widgets/widget-updated
  [state {:keys [widget-id name description]}]
  (update state widget-id merge {:name name :description description}))

(defmethod widgets* :widgets/widget-deleted
  [state {:keys [widget-id]}]
  (dissoc state widget-id))

(defmethod widgets* :default [state _] state)

(defreadmodel :widgets widgets
  {:events widget-event-types :version 1}
  [state event]
  (widgets* state event))

;; Query helpers
(defn get-widget [ctx widget-id]
  (get (rmp/project ctx :widgets/widgets {:tags #{[:widget widget-id]}})
       widget-id))

(defn get-all-widgets [ctx]
  (->> (vals (rmp/project ctx :widgets/widgets))
       (filter :active)
       (sort-by :name)))

Step 4: Implement Commands (core/commands.clj)

(ns ai.obney.orc.widget-service.core.commands
  (:require [ai.obney.orc.widget-service.core.read-models :as rm]
            [ai.obney.grain.event-store-v3.interface :refer [->event]]
            [ai.obney.grain.command-processor-v2.interface :refer [defcommand]]
            [cognitect.anomalies :as anom]))

(defcommand :widgets create-widget
  "Create a new widget."
  [{{:keys [name description]} :command :as ctx}]
  (let [widget-id (random-uuid)]
    {:command-result/events
     [(->event {:type :widgets/widget-created
                :tags #{[:widget widget-id]}
                :body {:widget-id widget-id
                       :name name
                       :description description}})]}))

(defcommand :widgets delete-widget
  "Delete a widget."
  [{{:keys [widget-id]} :command :as ctx}]
  (if-let [widget (rm/get-widget ctx widget-id)]
    {:command-result/events
     [(->event {:type :widgets/widget-deleted
                :tags #{[:widget widget-id]}
                :body {:widget-id widget-id}})]}
    {::anom/category ::anom/not-found
     ::anom/message "Widget not found."}))

Step 5: Implement Queries (core/queries.clj)

Queries return data — no UI rendering in a library.

(ns ai.obney.orc.widget-service.core.queries
  (:require [ai.obney.orc.widget-service.core.read-models :as rm]
            [ai.obney.grain.query-processor.interface :refer [defquery]]))

(defquery :widgets get-all
  "Get all active widgets."
  [ctx]
  {:query/result {:widgets (rm/get-all-widgets ctx)}})

(defquery :widgets get-by-id
  "Get a widget by ID."
  [{{:keys [widget-id]} :query :as ctx}]
  (let [widget (rm/get-widget ctx (parse-uuid widget-id))]
    (if widget
      {:query/result {:widget widget}}
      {:query/result {:widget nil}})))

Step 6: Create Interface (interface.clj)

(ns ai.obney.orc.widget-service.interface
  (:require ;; Side-effect requires — registers commands/queries/processors
            [ai.obney.orc.widget-service.core.commands]
            [ai.obney.orc.widget-service.core.queries]
            [ai.obney.orc.widget-service.core.todo-processors]
            ;; Re-export read model helpers
            [ai.obney.orc.widget-service.core.read-models :as rm]))

(def get-widget rm/get-widget)
(def get-all-widgets rm/get-all-widgets)

Step 7: Todo Processors (core/todo_processors.clj)

(ns ai.obney.orc.widget-service.core.todo-processors
  (:require [ai.obney.grain.todo-processor-v2.interface :refer [defprocessor]]))

;; Add defprocessor forms here when event-driven side effects are needed

Step 8: Write Tests

(ns ai.obney.orc.widget-service.commands-test
  (:require [clojure.test :refer [deftest testing is]]
            [ai.obney.orc.grain-test-utils.interface :as tu]
            [ai.obney.orc.widget-service.core.commands]
            [ai.obney.orc.widget-service.interface.schemas]
            [cognitect.anomalies :as anom]))

(deftest create-widget-test
  (testing "creates a widget"
    (tu/with-test-context [ctx]
      (let [result (tu/process-command! ctx
                     {:command/name :widgets/create-widget
                      :name "Test Widget"
                      :description "A test"})]
        (is (tu/event-of-type? result :widgets/widget-created))
        (let [event (tu/find-event result :widgets/widget-created)]
          (is (= "Test Widget" (:name event))))))))

Step 9: Register in deps.edn

Root deps.edn

Add to :aliases > :dev > :extra-deps:

orc/widget-service {:local/root "components/widget-service"}

projects/orc/deps.edn

Add to :deps:

orc/widget-service {:local/root "../../components/widget-service"}

Reference Files

  • components/orc-service/ — Core ORC service (comprehensive example)
  • components/colbert/ — ColBERT service (commands, read models, queries)
  • components/ontology/ — Ontology service (event sourcing patterns)

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

allium

Free

Give your AI agents something more useful than a prompt. Velocity through clarity.

The English summary is being prepared. Showing the original description.

ObneyAI/orc22Updated Oct 20, 2026

Extract an Allium specification from an existing codebase. Use when the user has existing code and wants to distil behaviour into a spec, reverse engineer a specification from implementation, generate a spec from code, turn implementation into a behavioural specification, or document what a codebase does in Allium terms.

The English summary is being prepared. Showing the original description.

ObneyAI/orc22Updated Oct 20, 2026

elicit

Free

Run a structured discovery session to build an Allium specification through conversation. Use when the user wants to create a new spec from scratch, elicit or gather requirements, capture domain behaviour, specify a feature or system, define what a system should do, or is describing functionality and needs help shaping it into a specification.

The English summary is being prepared. Showing the original description.

ObneyAI/orc22Updated Oct 20, 2026

Implement command handlers that validate state and emit events using the Grain framework

The English summary is being prepared. Showing the original description.

ObneyAI/orc22Updated Oct 20, 2026

Create scheduled background jobs that run on cron schedules

The English summary is being prepared. Showing the original description.

ObneyAI/orc22Updated Oct 20, 2026

Implement query handlers that read from event-sourced read models

The English summary is being prepared. Showing the original description.

ObneyAI/orc22Updated Oct 20, 2026

See all skills by ObneyAI

Report a problem with this skill