CeremonyPlugin replaces hardcoded cron tasks with configurable, observable, and hot-reloadable YAML-defined ceremonies. A ceremony is a recurring scheduled ritual — for example, a daily board health check, PR triage, or weekly sprint review.
What is a Ceremony?
A ceremony is a named, cron-scheduled task that:
- Fires on a cron schedule (e.g. every morning at 9am, every Friday at 5pm)
- Invokes an agent skill against one or more project targets
- Publishes lifecycle events on the EventBus
- Persists execution outcomes to
knowledge.db - Optionally notifies a Discord channel on completion
Ceremonies are defined in YAML files placed in workspace/ceremonies/. They are loaded at startup and hot-reloaded every 5 seconds when files change, with no restart required.
YAML Schema
Each ceremony is defined in its own .yaml file inside workspace/ceremonies/.
id: board.pr-audit # required — unique ceremony identifier
name: PR Audit # required — human-readable name
description: "…" # optional — free-form human-readable purpose
schedule: "0 9 * * 1-5" # required — cron expression (in `timezone`, else container TZ)
timezone: America/New_York # optional — IANA tz the cron is evaluated in
skill: audit-prs # required — agent skill to invoke
targets: # required — agent names or ['all']
- quinn
notifyChannel: "1469195…" # optional — Discord channel ID for outcome notifications
enabled: true # optional — defaults to true; set false to pause
createdBy: quinn # optional — owning agent for multi-tenant HTTP API gatingField Reference
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique identifier, e.g. board.pr-audit. Used in bus topics. |
name | string | yes | Human-readable name shown in logs and notifications. |
description | string | no | Free-form purpose string, surfaced in the API listing. |
schedule | string | yes | Standard 5-field cron expression, evaluated in timezone (or the process/container TZ if unset). |
timezone | string | no | IANA timezone the cron is evaluated in, e.g. America/Los_Angeles. Stays correct across DST (a 0 6 * * * + America/Los_Angeles fires at 6am PT year-round). Omit to use the container TZ. |
skill | string | yes | Agent skill invoked when the ceremony fires. |
targets | string[] | yes | Non-empty list of agent names (e.g. ['quinn']) or ['all'] for fleet broadcast. Passed as projectPaths in the ceremony.{id}.execute context for backwards-compatible consumer shape. |
notifyChannel | string | no | Discord channel ID for outcome notifications. |
enabled | boolean | no | Whether this ceremony is active. Defaults to true. |
createdBy | string | no | Owning agent name. Stamped server-side when created through the HTTP API with a per-agent key; gates update/delete to the owner (admins bypass). See HTTP API → per-agent keys. |
Cron Expression Examples
| Expression | Meaning |
|---|---|
0 9 * * 1-5 | 9:00 AM UTC, Monday–Friday |
0 17 * * 5 | 5:00 PM UTC on Fridays |
0 */3 * * * | Every 3 hours |
0 8 * * 1 | 8:00 AM UTC every Monday |
File Layout
workspace/
ceremonies/ # global ceremonies (all projects)
board-pr-audit.yaml
weekly-sprint-review.yaml
.proto/
projects/
{project-slug}/
ceremonies/ # project-scoped overrides
board-pr-audit.yaml # overrides global ceremony with same idProject-level ceremonies with the same id override global ones. All other global ceremonies are inherited unchanged.
How CeremonyPlugin Integrates with the Scheduler and Bus
At startup, CeremonyPlugin.install(bus):
- Loads all ceremony YAML files via
CeremonyYamlLoader - Schedules a cron timer for each enabled ceremony
- Starts a 5-second hot-reload polling loop
When a ceremony fires:
- Publishes
ceremony.{id}.executeon the EventBus - Publishes
agent.skill.requestto dispatch the skill to an agent - Waits up to 120 seconds for
agent.skill.response.{runId} - Publishes
ceremony.{id}.completedwith the outcome - Persists the outcome to
knowledge.db(capped at 500 entries per ceremony) - Sends a Discord notification if
notifyChannelis set
Bus Topics
| Topic | Direction | Description |
|---|---|---|
ceremony.{id}.execute | published | Fired when a ceremony's cron triggers |
ceremony.{id}.completed | published | Fired after a ceremony run finishes |
agent.skill.request | published | Dispatches the skill to an agent for execution |
agent.skill.response.{runId} | subscribed | Result from the agent skill execution |
ceremony.# | subscribed | Wildcard — used internally to intercept completed events |
ceremony.state.snapshot | published | Ceremony state update via CeremonyStateExtension (NOT world.state.#; see issue #424) |
Execute Payload (ceremony.{id}.execute)
{
type: "ceremony.execute",
context: {
runId: string, // UUID for this run
ceremonyId: string,
projectPaths: string[], // resolved targets
startedAt: number, // Unix ms
},
skill: string,
ceremonyName: string,
}Completed Payload (ceremony.{id}.completed)
{
type: "ceremony.completed",
outcome: {
runId: string,
ceremonyId: string,
skill: string,
status: "success" | "failure" | "timeout",
duration: number, // ms
targets: string[],
startedAt: number,
completedAt: number,
result?: string, // optional summary from skill
error?: string, // set on failure or timeout
}
}Built-in vs Custom Ceremonies
Built-in ceremonies ship as default YAML files in src/plugins/ceremonies/defaults/. On first run, CeremonyPlugin copies any missing defaults into workspace/ceremonies/. You can edit these files to customize them — they will not be overwritten on subsequent runs.
Custom ceremonies are any YAML files you add to workspace/ceremonies/ or .proto/projects/{slug}/ceremonies/. There is no registration step — just drop the file and it will be picked up within 5 seconds.
Outcome Persistence
Ceremony outcomes are stored in knowledge.db using CeremonyOutcomesRepository. Each ceremony retains up to 500 historical outcomes; older entries are pruned automatically. Status values are success, failure, and timeout (after 120 seconds with no skill response).