Skip to content
cccskills
FreePublished on GitHub

grain-periodic-task

Create scheduled background jobs that run on cron schedules

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

See how to install

Files included (1)

  • SKILL.md6.3 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Grain Periodic Task Pattern

Periodic tasks are scheduled background jobs that run on cron schedules. They use defperiodic to emit trigger events, and a corresponding defprocessor handles the actual work.

See Pattern Compendium section 1.4.1 for the authoritative reference.

Two-File Pattern

Periodic tasks are split into two parts:

  1. periodic_tasks.clj -- defperiodic emits a lightweight trigger event per tenant on schedule
  2. todo_processors.clj -- defprocessor subscribes to the trigger event and does the real work

The framework iterates over all tenants automatically. The defperiodic handler receives [tenant-id time] (NOT [context time]) and returns {:result/events [...]}. The framework appends the events per-tenant.

Periodic Task File (core/periodic_tasks.clj)

(ns ai.obney.orc.my-service.core.periodic-tasks
  "Scheduled trigger events for my-service."
  (:require [ai.obney.grain.event-store-v3.interface :refer [->event]]
            [ai.obney.grain.periodic-task.interface :refer [defperiodic]]))

(defperiodic :my-service send-reminders
  {:schedule {:cron "0 * * * *" :timezone "America/Chicago"}}
  "Emit reminder-check trigger for each tenant every hour."
  [_tenant-id time]
  {:result/events
   [(->event {:type :my-service/reminder-check-triggered
              :tags #{[:reminder-check (random-uuid)]}
              :body {:triggered-at (str time)}})]})

The generated var name is my-service-send-reminders.

Corresponding Processor (core/todo_processors.clj)

(ns ai.obney.orc.my-service.core.todo-processors
  (:require [ai.obney.orc.my-service.core.read-models :as rm]
            [ai.obney.grain.command-processor-v2.interface :as cp]
            [ai.obney.grain.time.interface :as time]
            [ai.obney.grain.todo-processor-v2.interface :refer [defprocessor]]
            [com.brunobonacci.mulog :as u]))

(defprocessor :my-service reminder-runner
  {:topics #{:my-service/reminder-check-triggered}}
  "Process pending reminders when trigger fires."
  [context]
  (let [items (rm/get-pending-items context)]
    (u/log ::reminder-runner :count (count items))
    (doseq [item items]
      (cp/process-command
       (assoc context :command {:command/id (random-uuid)
                                :command/timestamp (time/now)
                                :command/name :my-service/send-reminder
                                :item-id (:id item)})))
    {}))

Complete Example: Membership Billing

;; periodic_tasks.clj
(defperiodic :membership billing-check
  {:schedule {:cron "0 6 * * *" :timezone "America/Chicago"}}
  "Daily billing check trigger."
  [_tenant-id time]
  {:result/events [(->event {:type :membership/billing-check-triggered
                             :tags #{[:billing-check (random-uuid)]}
                             :body {:triggered-at (str time)}})]})

;; todo_processors.clj
(defprocessor :membership billing-runner
  {:topics #{:membership/billing-check-triggered}}
  "Process memberships due for billing."
  [context]
  (let [today (str (java.time.LocalDate/now))
        due (rm/get-memberships-due-for-billing context today)]
    (doseq [m due]
      (cp/process-command
       (assoc context :command {:command/id (random-uuid)
                                :command/timestamp (time/now)
                                :command/name :membership/initiate-billing
                                :membership-id (:membership-id m)})))
    {}))

Schedule Formats

FormatExampleDescription
Cron{:cron "0 * * * *" :timezone "America/Chicago"}Standard cron (minute hour day month weekday)

Common cron patterns:

  • "0 * * * *" -- every hour on the hour
  • "*/15 * * * *" -- every 15 minutes
  • "0 3 * * *" -- daily at 3 AM
  • "0 6 * * *" -- daily at 6 AM
  • "0 0 * * 1" -- weekly on Monday at midnight

CAS for Idempotency

When a trigger event must be processed at most once, use CAS inside the defprocessor:

(defprocessor :my-service digest-sender
  {:topics #{:my-service/digest-triggered}}
  "Send daily digest -- at most once per user per day."
  [context]
  (let [users (rm/get-users-needing-digest context)]
    (doseq [user users]
      (es/append (:event-store context)
                 {:tenant-id (:tenant-id context)
                  :events [(->event {:type :my-service/digest-sent
                                     :tags #{[:user (:user-id user)]}
                                     :body {:user-id (:user-id user)
                                            :date (str (java.time.LocalDate/now))}})]
                  :cas {:types #{:my-service/digest-sent}
                        :tags #{[:user (:user-id user)]}
                        :predicate-fn (fn [existing]
                                        (empty? (into [] existing)))}}))
    {}))

The CAS predicate checks that no :my-service/digest-sent event exists for this user with these tags. If another instance already emitted one, the append silently fails.

Auto-Registration and Wiring

Both defperiodic and defprocessor register in global registries automatically. No manual registry maps are needed.

Interface.clj only needs bare side-effect requires:

(ns ai.obney.orc.my-service.interface
  (:require [ai.obney.orc.my-service.core.commands]
            [ai.obney.orc.my-service.core.queries]
            [ai.obney.orc.my-service.core.todo-processors]   ;; bare require
            [ai.obney.orc.my-service.core.periodic-tasks]    ;; bare require
            [ai.obney.orc.my-service.core.read-models :as rm]))

;; No periodic-tasks or todo-processors re-export needed
(def get-item rm/get-item)

No manual wiring is needed. pt/start-periodic-triggers! discovers periodic tasks from the global registry. The control plane discovers todo processors the same way.

Reference Files

  • components/membership-service/src/.../core/periodic_tasks.clj -- Billing check trigger
  • components/scheduling-service/src/.../core/periodic_tasks.clj -- Scheduling triggers
  • components/user-service/src/.../core/periodic_tasks.clj -- User maintenance
  • docs/contributors/CONTRIBUTOR-GRAIN-PATTERNS.md section 1.4.1 -- Authoritative periodic task reference

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

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

Build read models by reducing events into queryable state

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