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
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):
Special syntax:
- 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
Thecrewship recurring command (cmd/crewship/cmd_admin_extras.go) offers list, create, update, and delete (there is no get, enable, or disable subcommand — enable/disable is the --enabled flag on recurring update). The CLI create / update take flags (--crew, --title, --cron, --project, --assignee, …) that resolve to entity IDs; the manifest instead resolves slugs and can express the full template in one file, so reach for crewship apply for repeatable, slug-based setups.
REST endpoint mapping
The endpoint is:
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.