一键导入
grain-todo-processor
Implement event-driven side effects that react to domain events
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Implement event-driven side effects that react to domain events
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Create scheduled background jobs that run on cron schedules
Build an ORC behavior tree workflow using the DSL
Set up LLM-as-judge evaluation for ORC workflows
Implement command handlers that validate state and emit events using the Grain framework
Create a complete Grain domain service with schemas, read models, commands, queries, and tests
Implement query handlers that read from event-sourced read models
| name | grain-todo-processor |
| description | Implement event-driven side effects that react to domain events |
Todo processors are policies that react to domain events and emit follow-up events or perform side effects.
(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]))
(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).
{:result/events [event1 event2]}
{:result/effect (fn [] (send-email! ...))
:result/checkpoint :after} ;; or :before
{}
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.
(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))}}})]})))
(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}})]}
{})))
(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)}
{})))
(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}}})]}))
:event, :event-store, :tenant-id{:result/events [...]} or {} (empty map):topics setcomponents/user-service/src/ai/obney/workshop/user_service/core/todo_processors.clj - Email notificationscomponents/crm-service/src/ai/obney/workshop/crm_service/core/todo_processors.clj - Attribution & duplicates