一键导入
grain-periodic-task
Create scheduled background jobs that run on cron schedules
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Create scheduled background jobs that run on cron schedules
用 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.
Implement command handlers that validate state, emit events, and return signals using the Grain framework
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-periodic-task |
| description | Create scheduled background jobs that run on cron schedules |
Periodic tasks are scheduled background jobs that run on cron or interval schedules. They operate outside the request/response cycle and are used for maintenance, cleanup, reminders, and other time-based operations.
In this compact app, periodic triggers are managed through the periodic-task interface
(ai.obney.grain.periodic-task.interface). A service exposes a periodic_tasks.clj
file with start/stop functions that wrap pt/start-periodic-triggers! and
pt/stop-periodic-triggers!. The root system wires this lifecycle through the
::periodic-triggers Integrant key in src/cjbarre/grain_todo_list.clj.
service/<svc>-service/periodic_tasks.clj)(ns cjbarre.grain-todo-list.service.<svc>-service.periodic-tasks
"Scheduled background triggers for <svc>-service."
(:require [ai.obney.grain.event-store-v3.interface :as es]
[ai.obney.grain.periodic-task.interface :as pt]))
(defn start
[{:keys [event-store tenant-id]}]
(pt/start-periodic-triggers!
{:append-fn (partial es/append event-store)
:tenant-ids-fn (constantly #{tenant-id})}))
(defn stop
[triggers]
(pt/stop-periodic-triggers! triggers))
pt/start-periodic-triggers! takes an :append-fn (used to append the trigger event
into the event store) and a :tenant-ids-fn returning the set of tenant ids to run for.
It returns a handle that pt/stop-periodic-triggers! shuts down. The individual triggers
(their schedules and the events they emit) are defined via the periodic-task interface;
prefer the real pt/start-periodic-triggers! / pt/stop-periodic-triggers! names over
any ad-hoc registry.
A periodic task may run twice (e.g. overlapping ticks or a restart). Make each run safe to repeat. Two strategies:
Use a compare-and-swap append so the work only happens once for a given key/period:
(require '[ai.obney.grain.event-store-v3.interface :as es :refer [->event]])
(es/append event-store
{:tenant-id tenant-id
:events [(->event {:type :todo/digest-sent
:tags #{[:user user-id]}
:body {:user-id user-id
:date (str (java.time.LocalDate/now))}})]
:cas {:types #{:todo/digest-sent}
:tags #{[:user user-id]}
:predicate-fn (fn [existing]
(empty? (into [] existing)))}})
The CAS predicate checks no :todo/digest-sent event already exists for this user with
these tags. If another run already emitted one, the append silently fails — at most once.
Alternatively, let the triggered command's own validation reject a duplicate (e.g. the command refuses if the entity is already in the target state). Either way, a task running twice must not double-apply its effect.
tenant-ids-fn returns the set of tenants the triggers run for. In this single-tenant
dev app it is (constantly #{tenant-id}). To enumerate tenants dynamically, read them
from the event store: es/tenants (from ai.obney.grain.event-store-v3.interface)
returns {tenant-id {:tenant/last-event-id uuid-or-nil}} — use (keys ...) for the ids.
The root system already wires periodic triggers via the ::periodic-triggers Integrant
key in src/cjbarre/grain_todo_list.clj:
;; in the system map:
::periodic-triggers {:event-store (ig/ref ::event-store)
:tenant-id tenant-id}
;; init / halt:
(defmethod ig/init-key ::periodic-triggers [_ config]
(periodic-tasks/start config))
(defmethod ig/halt-key! ::periodic-triggers [_ triggers]
(periodic-tasks/stop triggers))
with [cjbarre.grain-todo-list.service.todo-list-service.periodic-tasks :as periodic-tasks]
in the ns require. To add periodic tasks for another service, either add triggers to the
existing periodic-tasks/start, or add a new service periodic_tasks.clj and a parallel
Integrant key. After changing it, restart the system:
(app/stop app)
(def app (app/start))
src/cjbarre/grain_todo_list/service/todo_list_service/periodic_tasks.clj — working start/stopsrc/cjbarre/grain_todo_list.clj — the ::periodic-triggers Integrant wiring