Skip to content

feat: add support for recurring tasks - #1611

Open
Steven4Hooisma wants to merge 4 commits into
getprobo:mainfrom
Steven4Hooisma:Recurring-Tasks
Open

feat: add support for recurring tasks#1611
Steven4Hooisma wants to merge 4 commits into
getprobo:mainfrom
Steven4Hooisma:Recurring-Tasks

Conversation

@Steven4Hooisma

@Steven4Hooisma Steven4Hooisma commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
  • Introduced new fields for recurrence interval unit and count in task model.
  • Updated task creation and update requests to include recurrence parameters.
  • Implemented logic to handle overdue recurring tasks in the task worker.
  • Added validation to ensure both recurrence unit and count are set together.
  • Created new SQL migration for the recurrence interval unit type and count columns.
  • Enhanced GraphQL schema to support recurrence fields in task queries and mutations.

Summary by cubic

Add recurring tasks with interval unit and count across API, UI, CLI, and integrations. A background worker advances overdue recurring tasks; validations and errors are stricter and localized, and a migration stores recurrence data with DB constraints.

Tests +336 -0

  • e2e coverage for create/update with recurrence and validation failures via console GraphQL and MCP; unit tests for next-deadline calculation and worker claim/advance behavior.

Coredata +233 -39

  • Added task_recurrence_interval_unit enum and nullable recurrence_interval_unit/recurrence_interval_count columns with a check constraint; extended Task model/queries and loader to claim the next overdue recurring task with FOR UPDATE SKIP LOCKED.

GraphQL API +61 -27

  • Added Task.recurrenceIntervalUnit and Task.recurrenceIntervalCount plus enum and input fields; updated resolvers and types to pass recurrence fields through.

MCP +82 -31

  • Extended spec, types, and resolvers with recurrence fields (omittable supported); validations ensure unit/count pairing and require a deadline when recurring.

prb (CLI) +62 -16

  • Added --recurrence-unit and --recurrence-count to task create and task update; support clearing on update (count 0 clears) with flag validation and helpful errors.

Service +295 -31

  • Added recurring task worker that detaches overdue tasks and creates the next occurrence, collapsing missed cycles; registered in probod to run every 15 minutes; enforced create/update validation (pair unit/count, require deadline when recurring) with clearer errors.

Package: n8n-node +111 -0

  • Added recurrence unit/count options to task create/update operations and included fields in GraphQL payloads.

App: console +137 -9

  • Added recurrence fields to the Task form with Zod refinements and localized error messages; mutation support; recurring badge with tooltip in task rows and localized en-US/fr-FR strings for units, errors, and tooltips.

Written for commit d1a15c8. Summary will update on new commits.

Review in cubic

- Introduced new fields for recurrence interval unit and count in task model.
- Updated task creation and update requests to include recurrence parameters.
- Implemented logic to handle overdue recurring tasks in the task worker.
- Added validation to ensure both recurrence unit and count are set together.
- Created new SQL migration for the recurrence interval unit type and count columns.
- Enhanced GraphQL schema to support recurrence fields in task queries and mutations.
- Added tests for next recurrence deadline calculation and task worker functionality.

Signed-off-by: Steven4Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 issues found across 23 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="pkg/probo/recurring_task_worker.go">

<violation number="1" location="pkg/probo/recurring_task_worker.go:80">
P1: A malformed recurrence row can crash or wedge the recurring-task worker: `recurrence_interval_count` is nullable and unconstrained in the migration, while selection only requires a unit. Validate this invariant at the database boundary (and/or skip invalid rows) before dereferencing, since zero or negative counts also make `nextRecurrenceDeadline` loop forever while its transaction remains open.</violation>
</file>

<file name="pkg/cmd/task/update/update.go">

<violation number="1" location="pkg/cmd/task/update/update.go:139">
P2: Clearing either recurrence flag by itself fails for existing recurring tasks instead of clearing recurrence. Clear both interval fields when either flag requests a clear, or reject incomplete clear requests locally.</violation>
</file>

<file name="pkg/coredata/task.go">

<violation number="1" location="pkg/coredata/task.go:51">
P1: Measure imports now fail at `Task.Upsert`: strict `RowToStructByName[Task]` cannot find either new recurrence field in its `RETURNING` row. Include recurrence columns and args in the upsert insert/conflict clauses and both columns in `RETURNING`.</violation>
</file>

<file name="apps/console/src/components/tasks/TasksCard.tsx">

<violation number="1" location="apps/console/src/components/tasks/TasksCard.tsx:627">
P2: The recurring badge icon uses a `<span title="...">` to convey recurrence information, but screen readers won't reliably announce this. The embedded `<IconRotateCw />` SVG likely has no accessible name (no `aria-label`, `role="img"`), and the `<span>` isn't keyboard-focusable. Consider adding `aria-label` directly on the icon or using a visually-hidden text element for screen reader users, matching the pattern used by `<Button>` with `title` in this same component.</violation>
</file>

<file name="apps/console/src/_locales/fr-FR.json">

<violation number="1" location="apps/console/src/_locales/fr-FR.json:1429">
P2: The recurringBadge tooltip renders improperly in French for singular counts. `tooltip_one` should use a singular-aware template such as "Se répète chaque {{unit}}" instead of "Se répète tous les {{count}} {{unit}}", which is ungrammatical when count=1 ("tous les 1 jour").</violation>

<violation number="2" location="apps/console/src/_locales/fr-FR.json:1430">
P3: The recurring-task tooltip is grammatically incorrect in French for counts greater than one because both plural branches reuse the singular translated unit (`2 Semaine`, `2 Année`). Providing pluralized unit translations or unit-specific tooltip variants would make the badge readable for all recurrence units.</violation>
</file>

<file name="apps/console/src/components/tasks/TaskFormDialog.tsx">

<violation number="1" location="apps/console/src/components/tasks/TaskFormDialog.tsx:211">
P1: Non-recurring tasks cannot be submitted with the new defaults: the form starts with count `1` and no recurrence unit, while the new refinement treats that as an invalid count-without-unit combination. Clearing recurrence on an existing task has the same problem because the reset path restores `1`; initializing the count as `null` when no unit is present (or clearing it when the unit becomes `none`) would preserve the intended optional recurrence behavior.</violation>

<violation number="2" location="apps/console/src/components/tasks/TaskFormDialog.tsx:464">
P1: Opening/rendering this recurrence selector throws because Radix `Select.Item` disallows `value=""`. Use a non-empty sentinel option and map it to null/empty before schema validation and mutation submission.</violation>
</file>

<file name="packages/n8n-node/nodes/Probo/actions/task/update.operation.ts">

<violation number="1" location="packages/n8n-node/nodes/Probo/actions/task/update.operation.ts:234">
P1: Updating recurrence always sends the count as a GraphQL string, so values such as `3` fail `Int` input coercion before the mutation runs. Use a numeric n8n parameter (and preserve the empty default for unchanged) to send an integer.</violation>
</file>

<file name="e2e/console/task_test.go">

<violation number="1" location="e2e/console/task_test.go:983">
P2: Add a subtest that creates a task with a deadline and then updates it with recurrence fields, asserting that recurrenceIntervalUnit and recurrenceIntervalCount are set correctly in the response. This would catch bugs in the update resolver's handling of recurrence fields, which is currently only exercised through the error path.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

{
displayName: 'Recurrence Interval Count',
name: 'recurrenceIntervalCount',
type: 'string',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Updating recurrence always sends the count as a GraphQL string, so values such as 3 fail Int input coercion before the mutation runs. Use a numeric n8n parameter (and preserve the empty default for unchanged) to send an integer.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/n8n-node/nodes/Probo/actions/task/update.operation.ts, line 234:

<comment>Updating recurrence always sends the count as a GraphQL string, so values such as `3` fail `Int` input coercion before the mutation runs. Use a numeric n8n parameter (and preserve the empty default for unchanged) to send an integer.</comment>

<file context>
@@ -193,6 +193,54 @@ export const description: INodeProperties[] = [
+	{
+		displayName: 'Recurrence Interval Count',
+		name: 'recurrenceIntervalCount',
+		type: 'string',
+		displayOptions: {
+			show: {
</file context>

}

unit := *task.RecurrenceIntervalUnit
count := *task.RecurrenceIntervalCount

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A malformed recurrence row can crash or wedge the recurring-task worker: recurrence_interval_count is nullable and unconstrained in the migration, while selection only requires a unit. Validate this invariant at the database boundary (and/or skip invalid rows) before dereferencing, since zero or negative counts also make nextRecurrenceDeadline loop forever while its transaction remains open.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/probo/recurring_task_worker.go, line 80:

<comment>A malformed recurrence row can crash or wedge the recurring-task worker: `recurrence_interval_count` is nullable and unconstrained in the migration, while selection only requires a unit. Validate this invariant at the database boundary (and/or skip invalid rows) before dereferencing, since zero or negative counts also make `nextRecurrenceDeadline` loop forever while its transaction remains open.</comment>

<file context>
@@ -0,0 +1,166 @@
+			}
+
+			unit := *task.RecurrenceIntervalUnit
+			count := *task.RecurrenceIntervalCount
+			deadline := nextRecurrenceDeadline(*task.Deadline, unit, count, now)
+
</file context>

value={field.value ?? ""}
onValueChange={field.onChange}
>
<Option value="">{t("taskFormDialog.recurrenceIntervalUnits.none")}</Option>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Opening/rendering this recurrence selector throws because Radix Select.Item disallows value="". Use a non-empty sentinel option and map it to null/empty before schema validation and mutation submission.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/components/tasks/TaskFormDialog.tsx, line 464:

<comment>Opening/rendering this recurrence selector throws because Radix `Select.Item` disallows `value=""`. Use a non-empty sentinel option and map it to null/empty before schema validation and mutation submission.</comment>

<file context>
@@ -393,6 +438,39 @@ export default function TaskFormDialog(props: Props) {
+                      value={field.value ?? ""}
+                      onValueChange={field.onChange}
+                    >
+                      <Option value="">{t("taskFormDialog.recurrenceIntervalUnits.none")}</Option>
+                      <Option value="DAY">{t("taskFormDialog.recurrenceIntervalUnits.day")}</Option>
+                      <Option value="WEEK">{t("taskFormDialog.recurrenceIntervalUnits.week")}</Option>
</file context>

Comment thread pkg/coredata/task.go
TimeEstimate *time.Duration `db:"time_estimate"`
AssignedToID *gid.GID `db:"assigned_to_profile_id"`
Deadline *time.Time `db:"deadline"`
RecurrenceIntervalUnit *TaskRecurrenceIntervalUnit `db:"recurrence_interval_unit"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Measure imports now fail at Task.Upsert: strict RowToStructByName[Task] cannot find either new recurrence field in its RETURNING row. Include recurrence columns and args in the upsert insert/conflict clauses and both columns in RETURNING.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/coredata/task.go, line 51:

<comment>Measure imports now fail at `Task.Upsert`: strict `RowToStructByName[Task]` cannot find either new recurrence field in its `RETURNING` row. Include recurrence columns and args in the upsert insert/conflict clauses and both columns in `RETURNING`.</comment>

<file context>
@@ -37,20 +37,22 @@ import (
+		TimeEstimate            *time.Duration              `db:"time_estimate"`
+		AssignedToID            *gid.GID                    `db:"assigned_to_profile_id"`
+		Deadline                *time.Time                  `db:"deadline"`
+		RecurrenceIntervalUnit  *TaskRecurrenceIntervalUnit `db:"recurrence_interval_unit"`
+		RecurrenceIntervalCount *int                        `db:"recurrence_interval_count"`
+		Rank                    int                         `db:"rank"`
</file context>

measureId: task?.measure?.id ?? measureId ?? "",
deadline: task?.deadline?.split("T")[0] ?? "",
recurrenceIntervalUnit: task?.recurrenceIntervalUnit ?? "",
recurrenceIntervalCount: task?.recurrenceIntervalCount ?? 1,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Non-recurring tasks cannot be submitted with the new defaults: the form starts with count 1 and no recurrence unit, while the new refinement treats that as an invalid count-without-unit combination. Clearing recurrence on an existing task has the same problem because the reset path restores 1; initializing the count as null when no unit is present (or clearing it when the unit becomes none) would preserve the intended optional recurrence behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/components/tasks/TaskFormDialog.tsx, line 211:

<comment>Non-recurring tasks cannot be submitted with the new defaults: the form starts with count `1` and no recurrence unit, while the new refinement treats that as an invalid count-without-unit combination. Clearing recurrence on an existing task has the same problem because the reset path restores `1`; initializing the count as `null` when no unit is present (or clearing it when the unit becomes `none`) would preserve the intended optional recurrence behavior.</comment>

<file context>
@@ -170,6 +207,8 @@ export default function TaskFormDialog(props: Props) {
         measureId: task?.measure?.id ?? measureId ?? "",
         deadline: task?.deadline?.split("T")[0] ?? "",
+        recurrenceIntervalUnit: task?.recurrenceIntervalUnit ?? "",
+        recurrenceIntervalCount: task?.recurrenceIntervalCount ?? 1,
       },
     });
</file context>

Comment thread pkg/cmd/task/create/create.go
"delete": "Supprimer"
},
"recurringBadge": {
"tooltip_one": "Se répète tous les {{count}} {{unit}}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The recurringBadge tooltip renders improperly in French for singular counts. tooltip_one should use a singular-aware template such as "Se répète chaque {{unit}}" instead of "Se répète tous les {{count}} {{unit}}", which is ungrammatical when count=1 ("tous les 1 jour").

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/_locales/fr-FR.json, line 1429:

<comment>The recurringBadge tooltip renders improperly in French for singular counts. `tooltip_one` should use a singular-aware template such as "Se répète chaque {{unit}}" instead of "Se répète tous les {{count}} {{unit}}", which is ungrammatical when count=1 ("tous les 1 jour").</comment>

<file context>
@@ -1413,6 +1424,10 @@
       "delete": "Supprimer"
+    },
+    "recurringBadge": {
+      "tooltip_one": "Se répète tous les {{count}} {{unit}}",
+      "tooltip_other": "Se répète tous les {{count}} {{unit}}"
     }
</file context>

Comment thread apps/console/src/components/tasks/TaskFormDialog.tsx Outdated
Comment thread pkg/probo/recurring_task_worker.go
},
"recurringBadge": {
"tooltip_one": "Se répète tous les {{count}} {{unit}}",
"tooltip_other": "Se répète tous les {{count}} {{unit}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The recurring-task tooltip is grammatically incorrect in French for counts greater than one because both plural branches reuse the singular translated unit (2 Semaine, 2 Année). Providing pluralized unit translations or unit-specific tooltip variants would make the badge readable for all recurrence units.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/_locales/fr-FR.json, line 1430:

<comment>The recurring-task tooltip is grammatically incorrect in French for counts greater than one because both plural branches reuse the singular translated unit (`2 Semaine`, `2 Année`). Providing pluralized unit translations or unit-specific tooltip variants would make the badge readable for all recurrence units.</comment>

<file context>
@@ -1413,6 +1424,10 @@
+    },
+    "recurringBadge": {
+      "tooltip_one": "Se répète tous les {{count}} {{unit}}",
+      "tooltip_other": "Se répète tous les {{count}} {{unit}}"
     }
   },
</file context>

Signed-off-by: Steven Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
Signed-off-by: Steven4Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
Signed-off-by: Steven4Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant