TeamGriddeveloper
TeamGrid Developer

Recurring tasks

Create advanced task series, preview schedules, manage immutable versions, override occurrences, and recover asynchronous operations.

Recurring tasks are task automation, not calendar appointments. A recurrence series owns an immutable sequence of definition versions. Each definition contains a safe task template and a deterministic policy. Evaluating that policy creates occurrence-ledger entries; materialization creates ordinary TeamGrid tasks with read-only recurrence provenance.

Choose scopes and ownership

Read operations require task-recurrences:read and tasks:read. Preview, definition, lifecycle, occurrence, recheck and operation-cancellation writes require all of task-recurrences:write, tasks:read, and tasks:write. Only external trigger submission uses task-recurrences:run plus tasks:write. Workspace entitlement, task permissions, sharing, resource grants and series ownership are enforced in addition to credential scopes.

Workflow Required scopes
List/get series, versions, occurrences, stored previews and operation status task-recurrences:read, tasks:read
Create or preview a draft; update, pause, resume, end, archive, restore, transfer, recheck or remove a series from tasks task-recurrences:write, tasks:read, tasks:write
Restore a version; override, clear or retry an occurrence; cancel an operation task-recurrences:write, tasks:read, tasks:write
Submit an external event task-recurrences:run, tasks:write

Use a service account for a shared integration and a personal credential for user-owned automation. A guessed series or occurrence from another workspace remains indistinguishable from a missing resource.

Preview before creating

POST /task-recurrences/preview validates the complete template, references, policy complexity, time zone and daylight-saving behavior without creating a series. A small preview returns 200 and a taskRecurrencePreview. A permitted high-cost policy returns 202, a taskRecurrenceOperation, and its exact polling URL in Location.

With the SDK, handle both results explicitly:

const draft = await client.taskRecurrences.preview({
  count: 20,
  policy,
  template: { name: 'Weekly account review', projectId: 'project-id' },
})

const preview = draft.data.type === 'taskRecurrenceOperation'
  ? await client.taskRecurrenceOperations.wait(draft.data.id)
  : draft

Async draft payloads are encrypted and removed after the operation succeeds, fails or is cancelled. Operation status remains available for a bounded retention period. Poll through the returned regional client; do not construct a different cell URL.

Create a series from a task

The safest initial template is an existing task. Supply a stable idempotency key:

const created = await client.taskRecurrences.create({
  name: 'Weekly account review',
  sourceTaskId: 'task-id',
  policy: {
    candidates: {
      nodeId: 'weekly-review',
      op: 'calendarRule',
      rule: {
        byWeekDay: [1],
        frequency: 'weekly',
        interval: 1,
        startLocal: '2026-08-24T09:00:00',
      },
    },
    conditions: [],
    engineVersion: 'recurrence-v1',
    limits: { maxOccurrences: null, until: null },
    materialization: {
      catchUp: 'latest',
      lead: { unit: 'day', value: 0 },
      overlap: 'defer',
    },
    schemaVersion: 1,
    timeBasis: {
      disambiguation: 'compatible',
      mode: 'wall-clock',
      timeZone: 'Europe/Berlin',
    },
    transforms: [],
  },
}, { idempotencyKey: 'weekly-account-review-v1' })

Wall-clock rules preserve local time when UTC offset changes. Choose earlier, later or reject instead of compatible when ambiguous daylight-saving times need an explicit business decision. Elapsed-time rules preserve duration instead.

The policy AST also supports date and month sets, sequences, unions, intersections, differences, deduplication, occurrence/project/external events, offsets, business calendars, local-time mapping, exclusions, limits and conditions. Use the exact OpenAPI schema; unknown fields, unbounded nesting, duplicate node IDs and unsupported values fail closed.

Calendar rules with impossible month dates default to invalidDayHandling: "omit". Set previous-valid-day or next-valid-day only when that movement is the intended business rule. Duration and offset values are whole numbers. Business-day durations use the referenced working calendar, holidays and configured absences rather than elapsed 24-hour blocks.

Materialization policy is explicit:

  • catchUp can be none, latest, all, or bounded; bounded requires catchUpLimit from 1 through 100.
  • overlap can be allow, defer, skip, latest-only, or pause-series.
  • lead controls how far before the scheduled occurrence the ordinary task is created.

Paused, suspended and attention-required series retain accepted trigger events but do not enter the active materialization batch. Resume applies the selected catch-up rule; ended and archived series reject new trigger events. Event ingress/evaluation is bounded to 90 days. The newest-first search for latest and bounded catch-up expands through a five-year engine horizon so a stale series cannot trigger an unbounded foreground scan.

For a completion-relative source, omitting completionMode is equivalent to first-completion-only. Reopening and completing the same task does not create another successor. Set completionMode: "every-completion-transition" only when each distinct reopen/complete cycle must create one successor; transport retries of the same transition remain idempotent.

Update without rewriting history

Read the series and use its strong ETag for every mutation:

const current = await client.taskRecurrences.get(created.data.id)
const updated = await client.taskRecurrences.update(
  current.data.id,
  { changeReason: 'Move future reviews to Tuesday', policy: nextPolicy },
  { ifMatch: current.transport.headers.etag! },
)

An update creates a new immutable definition. effectiveFromOccurrenceKey on version resources records the first replaced non-materialized occurrence when one exists. Already generated tasks stay unchanged. The runtime supersedes and deterministically replans only non-materialized entries. A stale ETag returns 412; re-read and decide whether to merge.

Use version list/get/restore for audited rollback. Restore creates another new version rather than mutating the selected historical definition.

Change only one occurrence

List or preview occurrences to obtain the stable occurrenceKey. The occurrence ETag is separate from the series ETag:

const occurrence = await client.taskRecurrenceOccurrences.get(seriesId, occurrenceKey)
await client.taskRecurrenceOccurrences.override(
  seriesId,
  occurrenceKey,
  {
    action: 'materialize',
    scheduledForLocal: '2026-09-01T14:30:00',
    templatePatch: { name: 'Special quarterly review' },
  },
  { ifMatch: occurrence.transport.headers.etag! },
)

An override can move, skip or safely patch one not-yet-materialized occurrence. Clear the override to return it to the series plan. Retry only blocked, failed or skipped work after correcting its cause. A definition change invalidates a stale occurrence mutation rather than applying a new patch to an old template.

For a future item that is visible in previewStored but has no ledger resource yet, use its opaque placeholderToken and the exclusive create precondition. The token is short-lived and bound to the workspace, series, occurrence key and active immutable definition:

const preview = await client.taskRecurrences.previewStored(seriesId, { count: 20 })
const future = preview.data.attributes.occurrences[0]
await client.taskRecurrenceOccurrences.override(
  seriesId,
  future.occurrenceKey,
  { action: 'skip', placeholderToken: future.placeholderToken! },
  { createIfMissing: true },
)

HTTP callers send If-None-Match: * for this create path. Existing occurrences still require exactly one current If-Match: "tro1-…"; sending both headers fails before the internal mutation.

Lifecycle and recovery

Pause prevents new materialization; resume explicitly revalidates current permissions, entitlement, owner and references. Archive can be restored only as paused. End is terminal. Unavailable users, projects, lists, groups, tags, services or custom fields place a series into suspended or needs_attention instead of silently changing its task destination.

Recheck returns 202. Poll or wait for its operation:

const accepted = await client.taskRecurrences.recheck(seriesId)
const terminal = await client.taskRecurrenceOperations.wait(accepted.data.id, {
  maxWaitMs: 300_000,
  pollIntervalMs: 1_000,
})

CLI automation can use the same boundary:

teamgrid task-recurrences recheck SERIES_ID --wait --output json
teamgrid task-recurrence-operations wait OPERATION_ID --output json

Remove a recurrence from its tasks

Ending a series stops future work but leaves recurrence provenance on tasks that were already materialized. Use the separate compare-and-set operation when the user explicitly wants those tasks to become ordinary tasks again:

const current = await client.taskRecurrences.get(seriesId)
const removed = await client.taskRecurrences.removeFromTasks(seriesId, {
  ifMatch: current.transport.headers.etag!,
})
teamgrid task-recurrences remove-from-tasks SERIES_ID \
  --if-match '"tr1-<current revision>"' \
  --yes \
  --output json

The matching HTTP operation is POST /v1/task-recurrences/{id}/remove-from-tasks. It requires the latest strong series ETag and returns the ended series with a new ETag. The mutation is atomic across the series, materialized tasks and occurrence ledger:

  • the series becomes terminal ended, so it cannot be resumed or restored;
  • generated tasks are not deleted, archived or otherwise changed, but all server-managed recurrence-link fields are removed;
  • occurrence history remains immutable and auditable;
  • a detached occurrence exposes cardId: null, its former task as detachedCardId, and the detachedAt and detachedBy audit fields;
  • the now-ordinary task can seed a different recurrence through the normal create workflow.

A stale ETag returns 412, a missing one returns 428, and insufficient access fails without a partial detach. The SDK requires ifMatch; the CLI additionally requires an interactive confirmation or explicit --yes. The operation is deliberately unavailable through MCP.

Event-driven series

An externalEvent source subscribes a series to an exact eventType and optional sourceId. Submit a bounded envelope to POST /task-recurrences/{id}/events. eventId is the idempotency identity: replaying the same envelope is safe, while reusing it with different content returns a conflict. Event timestamps are bounded by the documented retention and future-skew window.

Use signed webhooks for low-latency output notifications and the change feed with resourceTypes=taskRecurrence for durable series reconciliation. Generated tasks are ordinary task resources and therefore use the task feed. Occurrence-ledger rows are read through their dedicated endpoints and are not a change-feed resource type. An event acceptance response means the trigger is durably queued; it does not promise that its generated task already exists.

MCP boundary

The MCP server exposes exactly seven bounded read-only recurrence tools: teamgrid_task_recurrences_list, teamgrid_task_recurrence_get, teamgrid_task_recurrence_preview, teamgrid_task_recurrence_versions_list, teamgrid_task_recurrence_version_get, teamgrid_task_recurrence_occurrences_list, and teamgrid_task_recurrence_occurrence_get. It deliberately excludes unsaved drafts, lifecycle writes, detachment, overrides, retries, event ingress and operation control. Use the API, SDK or CLI for those actions.

Stable documentation · Developer Experience · Reviewed 2026-08-19Edit this page ↗
Documentation feedbackWas this page useful?
Esc

Search TeamGrid Developer

Find guides, concepts and every API operation.