ワンクリックで
grain-command-handler
Implement command handlers that validate state, emit events, and return signals using the Grain framework
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Implement command handlers that validate state, emit events, and return signals using the Grain framework
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Give your AI agents something more useful than a prompt. Velocity through clarity.
Visual quality gate — screenshot each primary screen, critique it against the baseline rubric, and refine the components until it passes. Use before declaring a build done.
Create scheduled background jobs that run on cron schedules
Implement query handlers that read from event-sourced read models and render hiccup
Build read models by reducing events into queryable state with defreadmodel
Define Malli schemas for commands, events, and queries using defschemas
| name | grain-command-handler |
| description | Implement command handlers that validate state, emit events, and return signals using the Grain framework |
(ns cjbarre.grain-todo-list.service.todo-list-service.commands
(:require [ai.obney.grain.command-processor-v2.interface :refer [defcommand]]
[ai.obney.grain.command-processor-v2.interface :as cp]
[ai.obney.grain.event-store-v3.interface :refer [->event]]
[cjbarre.grain-todo-list.service.todo-list-service.read-models :as rm]
[cognitect.anomalies :as anom]))
(defcommand :todo capture-task
{:authorized? can-capture-task?}
"Add a new task."
[{{:keys [task-id title project-id]} :command :as ctx}]
;; 1. Read current state from read models (pass ctx, NOT bare event-store)
(let [task-id (or task-id (random-uuid))
user-id (current-user-id ctx)
project (when project-id (require-project ctx project-id))]
(cond
;; Return anomaly for validation failures
(contains? project ::anom/category) project
(and project (not= :active (:status project)))
{::anom/category ::anom/conflict
::anom/message "Cannot assign a task to an inactive project."}
;; Success: return events + optional signals
:else
{:command-result/events
[(->event
{:type :todo/task-captured
:tags #{[:task task-id] [:user user-id]}
:body (cond-> {:user-id user-id
:task-id task-id
:title title
:status :active
:order (next-order (rm/active-tasks ctx))}
project-id (assoc :project-id project-id))})]
:datastar/signals {:__toast "Task added."}})))
The {:authorized? auth-fn} metadata map goes between the namespace keyword and
the (optional) docstring.
Hybrid-auth rule: a command that writes or touches per-user /
account-scoped data MUST use a real predicate — never (constantly true) on
a mutation. (constantly true) is only for genuinely public, non-account
actions (e.g. sign-up / login before there is a user).
In this repo, predicates are small named fns over ctx. authenticated? checks
for a current user; ownership predicates verify the entity belongs to the caller:
(defn authenticated? [ctx]
(some? (:current-user/id ctx)))
(defn can-capture-task?
[{{:keys [project-id]} :command :as ctx}]
(and (authenticated? ctx)
(or (nil? project-id)
(owns-project? ctx project-id))))
(defn can-use-task?
[{{:keys [task-id]} :command :as ctx}]
(owns-task? ctx task-id)) ;; verifies the task is in the caller's projection
;; Public — ONLY for non-account-scoped actions (sign-up / login)
(defcommand :user sign-up
{:authorized? (constantly true)}
"..." [ctx] ...)
;; Requires login — the default for anything per-user/account-scoped
(defcommand :todo create-project
{:authorized? authenticated?}
"..." [ctx] ...)
;; Ownership check — for mutating an existing per-user entity
(defcommand :todo rename-task
{:authorized? can-use-task?}
"..." [ctx] ...)
{:command-result/events
[(->event {:type :todo/task-completed
:tags #{[:task task-id] [:user user-id]}
:body {:user-id user-id :task-id task-id}})]}
The action handler sends these signals back to the client via SSE patch:
{:command-result/events [...]
:datastar/signals {:__toast "Task added."}}
Special signal prefixes:
__toast -- show a brief notification__redirect -- navigate to a URL__refresh -- force page refreshDo NOT return form-field-reset signals from a command handler. Clearing an
input (e.g. emptying a :title signal after submit) is UI behavior and belongs
in the component's effect via ds-ui/reset-signal, not in the command result.
Command results carry domain outcomes (__toast, __redirect), not view state.
Just return the anomaly. The action handler auto-converts it to {:error "msg"}
signal, which form-error displays. Schema validation errors also produce
:fieldErrors automatically.
{::anom/category ::anom/conflict
::anom/message "Only active tasks can be completed."}
;; -> auto-converted to signal: {:error "Only active tasks can be completed."}
Prevents races by checking state hasn't changed between read and write:
{:command-result/events [...]
:command-result/cas {:types task-event-types
:predicate-fn (fn [_events]
(= :active (:status (rm/get-task ctx task-id))))}}
(let [result (cp/process-command
(assoc ctx :command {:command/name :todo/complete-task
:task-id task-id}))]
;; result is either an anomaly or {:command-result/events [...]}
result)
::anom/not-found -- resource does not exist::anom/conflict -- business rule violation (duplicate, invalid state)::anom/incorrect -- bad input data::anom/forbidden -- not authorized for this actionctx (the full context), not bare event-store#{[:task task-id] [:user user-id]} for filtered reads & ownership:user-id in event bodies for per-user data{:authorized? auth-fn} metadata between the namespace keyword and the docstringds-ui/reset-signal)src/cjbarre/grain_todo_list/service/user_service/commands.clj -- User auth commands (sign-up, login, password reset) — best example for auth predicatessrc/cjbarre/grain_todo_list/service/todo_list_service/commands.clj -- Todo commands (capture/rename/complete tasks, projects, weekly review)