Skip to main content

Recurring Issues

A recurring issue is a template that produces a fresh backlog issue on every cron tick: same title, description, priority, project, milestone, assignee, and labels. The intended workflow is “weekly bug triage”, “daily standup digest”, “monthly cost review” — anything you’d otherwise re-type as an issue on a schedule. Implementation: internal/api/recurring_issue_handler.go. Backed by the recurring_issues table. The cron expression is parsed and validated at write time using the standard 5-field syntax (Minute | Hour | DayOfMonth | Month | DayOfWeek); the parser is github.com/robfig/cron/v3. The next fire time is computed on create and on every cron change and stored in next_run, so the dispatcher can wake by index without re-parsing. next_run is always computed in UTC — including the very first fire — so it matches how the dispatcher advances the schedule; a non-UTC server no longer interprets the first fire differently from every subsequent one. Creator attribution. The user who creates the template is recorded on the row (created_by). Every fired issue is then attributed to that user (created_by_user_id), so the Issues API surfaces the creator instead of omitting it. Templates created before attribution was captured leave the creator unset and the fired issue omits it. Fire idempotency. Each occurrence reserves a durable key (sha256(kind‖id‖next_run-bucket)) in the shared pipeline_run_idempotency table before inserting, so two dispatcher replicas racing the same due row create the issue exactly once — the same scheme the pipeline scheduler uses.
All endpoints require an authenticated session and workspace context.

Endpoints

Recurring issue shape

Endpoint reference

CRUD over the templates. Cron is parsed and next_run recomputed on every create and cron change.

GET /api/v1/recurring-issues

List recurring issues in the workspace, newest first. Auth: authenticated session + workspace context. Query parameters: Response: 200 OK — JSON array (never null).

POST /api/v1/recurring-issues

Create a new recurring issue template. Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role (requireRole("create")). Request body: enabled is forced to true on create; flip it with PATCH. Response: 201 Created with the full recurring issue object. WebSocket event: recurring_issue.created broadcast on the workspace channel.

PATCH /api/v1/recurring-issues/{recurringId}

Partial update. When cron_expression is changed, the new value is re-parsed and next_run is recomputed in the same write — so the dispatcher’s next wake reflects the new schedule without a second round-trip. Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role. Request body: every field optional. Response: 200 OK with the full updated recurring issue object (with crew_name re-joined from crews). WebSocket event: recurring_issue.updated broadcast on the workspace channel.

DELETE /api/v1/recurring-issues/{recurringId}

Hard delete. The recurring template is removed; already-fired issues are not touched — they remain in the backlog with whatever state they’ve reached. Request: No request body. The {recurringId} path parameter identifies the recurring-issue template.
Hard delete, not soft delete — the template row is removed outright. Already-fired backlog issues survive untouched.
Auth: authenticated session + workspace context + OWNER or ADMIN role (requireRole("manage")). Response: 204 No Content. WebSocket event: recurring_issue.deleted broadcast on the workspace channel.

Cron syntax cheatsheet

5-field expression: <minute> <hour> <day-of-month> <month> <day-of-week>. All times are interpreted in UTC. There is currently no per-workspace timezone offset — the dispatcher schedules from next_run directly.

See also

  • Issues — the missions table where each fire lands.
  • Triage Rules — auto-route the newly-created backlog issue to a crew / agent.
  • Crews — the crew_id target.
  • Milestones — optional milestone_id target.