Skip to content
Back to skills

Grain Periodic Task

ASecurity

Create scheduled background jobs that run on cron schedules

  • 20 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 12, 2026
documentationgojava

Security analysis

A100/100

Scanned September 12, 2026

npx -y skills add ObneyAI/orc --skill grain-periodic-task --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Grain Periodic Task?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Grain Periodic Task
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/obneyai-grain-periodic-task/badge)](https://www.skillsdirectory.com/skills/obneyai-grain-periodic-task)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: grain-periodic-task
description: Create scheduled background jobs that run on cron schedules
---

# 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`)

```clojure
(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`)

```clojure
(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

```clojure
;; 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

| Format | Example | Description |
|--------|---------|-------------|
| 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`:

```clojure
(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:

```clojure
(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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.