Skip to content
cccskills
FreePublished on GitHub

grain-todo-processor

Implement event-driven side effects that react to domain events

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

See how to install

Files included (1)

  • SKILL.md5.9 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Grain Todo Processor Pattern

Todo processors are policies that react to domain events and emit follow-up events or perform side effects.

Required Imports

(ns ai.obney.orc.my-service.core.todo-processors
  (:require [ai.obney.orc.my-service.core.read-models :as rm]
            [ai.obney.grain.event-store-v3.interface :refer [->event]]
            [ai.obney.grain.todo-processor-v2.interface :refer [defprocessor]]
            [ai.obney.grain.time.interface :as time]))

Processor Template

(defprocessor :ns processor-name
  {:topics #{:ns/triggering-event}}
  "Docstring explaining what this processor does and when."
  [context]
  (let [event (:event context)
        entity-id (:entity-id event)
        ;; Check current state using read models
        entity (rm/get-entity context entity-id)]
    (if (should-take-action? entity)
      ;; Emit follow-up events
      {:result/events
       [(->event
         {:type :ns/follow-up-action
          :tags #{[:entity entity-id]}
          :body {:entity-id entity-id
                 :triggered-by (:event/type event)
                 :processed-at (str (time/now))}})]}
      ;; No action needed
      {})))

The generated var name is ns-processor-name (e.g., (defprocessor :crm ensure-attribution ...) creates var crm-ensure-attribution).

Return Patterns

Emit Events (pure -- batch checkpointed)

{:result/events [event1 event2]}

Side Effect with Checkpoint Control

{:result/effect (fn [] (send-email! ...))
 :result/checkpoint :after}  ;; or :before

No Action

{}

Auto-Registration

defprocessor registers the processor in a global registry automatically. There is no manual registry map (def todo-processors) needed. The control plane discovers processors from the registry and starts them when a tenant is assigned to the node.

Interface.clj only needs a bare side-effect require to load the namespace:

(ns ai.obney.orc.my-service.interface
  (:require ;; Side-effect requires
            [ai.obney.orc.my-service.core.commands]
            [ai.obney.orc.my-service.core.queries]
            [ai.obney.orc.my-service.core.todo-processors]  ;; bare require, no alias
            ...))

;; No todo-processors re-export needed

No manual wiring is needed -- the control plane discovers processors from the global registry.

Common Use Cases

Ensure Default Value

(defprocessor :crm ensure-attribution
  {:topics #{:crm/contact-created}}
  "Record default attribution if none exists."
  [context]
  (let [{:keys [contact-id]} (:event context)
        existing (rm/get-contact context contact-id)]
    (if (:attribution existing)
      {}  ;; Already has attribution
      {:result/events
       [(->event
         {:type :crm/attribution-recorded
          :tags #{[:contact contact-id]}
          :body {:contact-id contact-id
                 :attribution {:source :manual_entry
                               :recorded-at (str (time/now))}}})]})))

Duplicate Detection

(defprocessor :crm check-for-duplicates
  {:topics #{:crm/contact-created}}
  "Check for duplicates when contact is created."
  [context]
  (let [{:keys [contact-id]} (:event context)
        email (normalize-email (get-in (:event context) [:field-values :email]))
        existing (rm/get-contacts-by-email context email)]
    (if (seq (remove #(= contact-id (:id %)) existing))
      {:result/events
       [(->event
         {:type :crm/duplicate-detected
          :tags #{[:contact contact-id]}
          :body {:contact-id contact-id
                 :match-type :email
                 :match-value email}})]}
      {})))

Cascade Operations

(defprocessor :crm end-relationships-on-archive
  {:topics #{:crm/contact-archived}}
  "End all relationships when contact is archived."
  [context]
  (let [{:keys [contact-id]} (:event context)
        rels (rm/get-relationships-for-contact context contact-id)
        active-rels (filter #(nil? (:end-date %)) rels)]
    (if (seq active-rels)
      {:result/events
       (mapv (fn [rel]
               (->event
                {:type :crm/relationship-ended
                 :tags #{[:relationship (:id rel)]}
                 :body {:relationship-id (:id rel)
                        :end-date (str (java.time.LocalDate/now))
                        :reason "Contact archived"}}))
             active-rels)}
      {})))

Transform Source Data

(defprocessor :crm record-lead-attribution
  {:topics #{:crm/lead-captured}}
  "Map lead form type to attribution source."
  [context]
  (let [{:keys [contact-id form-type form-id]} (:event context)
        source (case form-type
                 :application :application_form
                 :intake :intake_form
                 :interest :interest_form
                 :manual_entry)]
    {:result/events
     [(->event
       {:type :crm/attribution-recorded
        :tags #{[:contact contact-id]}
        :body {:contact-id contact-id
               :attribution {:source source
                             :form-id form-id}}})]}))

Conventions

  • Processors receive a context map with :event, :event-store, :tenant-id
  • Use read models to check current state before acting
  • Return {:result/events [...]} or {} (empty map)
  • One processor can listen to multiple topics via the :topics set
  • Multiple processors can listen to the same topic
  • Normalize data before comparison (email lowercase, phone digits only)
  • Include reason/context in generated events for audit trail

Reference Files

  • components/user-service/src/ai/obney/workshop/user_service/core/todo_processors.clj - Email notifications
  • components/crm-service/src/ai/obney/workshop/crm_service/core/todo_processors.clj - Attribution & duplicates

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 8, 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 8, 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 8, 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 8, 2026

Create scheduled background jobs that run on cron schedules

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

ObneyAI/orc22Updated Oct 8, 2026

Implement query handlers that read from event-sourced read models

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

ObneyAI/orc22Updated Oct 8, 2026

See all skills by ObneyAI

Report a problem with this skill