kind: RecurringIssue
What it is
ARecurringIssue is a workspace-scoped, crew-owned schedule that
stamps out a fresh issue every time its cron expression fires. It is
the declarative equivalent of opening the same recurring task in
your project tracker every week — “weekly status review”, “monthly
billing reconciliation”, “daily on-call handoff” — except authored
in YAML, version-controlled, and applied through
crewship apply --file recurring.yaml.
Every recurring issue must belong to a specific crew (the crew_slug
field is required). The cron and timezone fields drive the
scheduler; the nested template: block describes the issue that
gets created on each fire.
YAML schema
Field reference
| Field | Type | Required | Notes |
|---|---|---|---|
spec.enabled | bool | no | Defaults to true. Set false to register the schedule but skip firing. |
spec.cron | string | yes | Standard 5-field cron expression. See syntax below. |
spec.timezone | string | yes | IANA timezone name (e.g. Europe/Prague, UTC, America/New_York). |
spec.template.title | string | yes | Issue title. Go template syntax ({{.Date}}) is interpreted at fire time. |
spec.template.description | string | no | Issue body. Same template syntax. |
spec.template.labels | string[] | no | List of Label slugs (or names) to attach. Each must exist in the workspace. |
spec.template.project_slug | string | no | Project slug. Must exist in the workspace. |
spec.template.priority | enum | no | One of none, low, medium, high, urgent. Default none. |
spec.template.assignee_agent_slug | string | no | Agent slug to assign the new issue to. |
spec.template.crew_slug | string | yes | Crew that owns the issue. Required — recurring issues are crew-scoped. |
Cron syntax
spec.cron uses the standard 5-field cron expression parsed by
github.com/robfig/cron/v3,
which mirrors the dialect of crontab(5):
| Expression | Meaning |
|---|---|
* * * * * | Every minute |
0 * * * * | Every hour, on the hour |
0 9 * * MON | Every Monday at 09:00 |
0 0 1 * * | First of every month, midnight |
*/15 9-17 * * MON-FRI | Every 15 minutes during business hours, Mon–Fri |
0 0 1 1 * | Once a year (Jan 1, midnight) |
- Lists:
1,15,30— minutes 1, 15, 30 - Ranges:
1-5— Monday through Friday (in DOW) - Steps:
*/5— every 5th unit - Names:
JAN,FEB, …,MON,TUE, … (case-insensitive)
@yearly, @monthly, @hourly
descriptor shortcuts — write the equivalent cron string instead
(0 0 1 1 * for yearly, etc.). It also does not support seconds
(no 6-field form) because the existing server handler uses the same
5-field parser; staying in lockstep prevents a manifest from
validating client-side and then failing server-side.
The timezone is independent of the system’s TZ — 0 9 * * MON with
timezone: Europe/Prague fires at 09:00 Prague time regardless of
where the Crewship server runs.
Examples
Minimal — daily standup reminder
Realistic — weekly review with labels and assignee
Cross-kind references in one apply
A singlecrewship apply --file can declare every dependency the
recurring issue needs:
CLI reference
The standalonecrewship recurring command (cmd/crewship/cmd_admin_extras.go) is intentionally minimal — list + delete only. Per-row create/update/enable/disable is manifest-driven: there is no crewship recurring create / get / enable / disable subcommand today; reach for crewship apply instead.
| Command | Description |
|---|---|
crewship recurring list | List recurring-issue schedules in the current workspace. |
crewship recurring delete <id> | Delete one schedule by row id. |
crewship apply --file recurring.yaml | Declarative create / update / delete — the only path for authoring schedules. Toggling spec.enabled: true|false in the manifest is how you enable/disable. |
crewship apply --file recurring.yaml --dry-run | Plan-only — shows the per-row create/update/delete the apply would perform. |
crewship export workspace | Includes every recurring issue the user can read. |
REST endpoint mapping
| Manifest field | POST body field | DB column |
|---|---|---|
metadata.name | name | (not stored as a column today — sent for symmetry with other kinds) |
metadata.slug | slug | (idempotency key; reads via list filter) |
metadata.description | description | n/a (kept on manifest side only) |
spec.enabled | enabled | enabled |
spec.cron | cron | cron_expression |
spec.timezone | timezone | timezone |
spec.template.title | template_json.title | title (also mirrored into template_json blob) |
spec.template.description | template_json.description | description |
spec.template.labels[] | template_json.label_ids[] (slug → id resolved) | labels_json |
spec.template.project_slug | template_json.project_id (slug → id) | project_id |
spec.template.priority | template_json.priority | priority |
spec.template.assignee_agent_slug | template_json.assignee_agent_id (slug → id) | assignee_id (with assignee_type='agent') |
spec.template.crew_slug | template_json.crew_id (slug → id) | crew_id |
template_json string field carrying the
resolved template; the server unmarshals it into the per-column
fields (title, description, labels_json, etc.) defined by the
existing recurring-issues table. Keeping the manifest payload
shaped as a single blob means future template fields don’t require
DB migrations or handler changes.
Validation rules
metadata.slugis required.spec.cronis required and must parse viacron.NewParser(cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow).spec.timezoneis required and must parse viatime.LoadLocation(i.e. a valid IANA zone).spec.template.titleis required (issues need a title).spec.template.crew_slugis required and must reference a crew that exists in the workspace (declared in this manifest or already on the server).spec.template.project_slug, if set, must reference an existing project.spec.template.assignee_agent_slug, if set, must reference an existing agent.- Every entry in
spec.template.labels[]must reference an existing label. spec.template.priority, if set, must be one ofnone,low,medium,high,urgent.
Apply behavior
Default mode (ApplyUpsert):
- Look up the existing row by
metadata.slugviaGET /api/v1/recurring-issues. - If absent →
Action=Create→POST /api/v1/recurring-issues. - If present and any field drifts →
Action=Update→PATCH /api/v1/recurring-issues/{id}. - If present and identical (including the resolved
template_json) →Action=Unchanged, no network call.
ApplyStrict: Same as Upsert but a pre-existing slug aborts
the apply with a clear error — useful in CI when the manifest must
create fresh resources.
ApplyReplace: Emits a Delete plan item followed by a
Create. Destructive; the apply path prompts for confirmation
unless --yes was passed.
Drift detection looks inside template_json — adding or
removing a single label surfaces as Action=Update even if every
top-level column (cron, timezone, enabled) matches. The diff
compares resolved IDs, not slugs, so reordering a labels list in
the YAML without semantic change produces an Unchanged plan
(label IDs are sorted before comparison).
Round-trip via export
crewship export workspace calls
ExportRecurringIssues(ctx, client) once per workspace, which:
- GETs
/api/v1/recurring-issuesfor the row list. - GETs
/api/v1/crews,/api/v1/projects,/api/v1/agents,/api/v1/labelsonce to build id → slug lookup tables. - For each row, unmarshals
template_json, reverse-resolves each ID back to its slug, and emits aRecurringIssueDocument.
crewship apply, producing
zero diff. Labels are sorted alphabetically in the exported file
so successive exports of the same state produce byte-identical
output.
crewship export crew <slug> filters: only recurring issues whose
template.crew_slug == <slug> are included, so a per-crew bundle
ships exactly the schedules that crew owns.
See also
kind: Crew— provides thecrew_slugreference. Crew must exist before the recurring issue applies.kind: Label— provides thelabels[]references.kind: Project— provides theproject_slugreference.kind: Routine— for cron-triggered automation pipelines, where the trigger runs code instead of opening an issue. Recurring issues are the lightweight cousin: they only create a tracked work item; routines run a full agent pipeline.kind: TriageRule— to auto-label or auto-route the issues a recurring schedule creates.