Skip to content

feat(automation): card_due_reached trigger + in-process scheduler#69

Merged
Musiker15 merged 1 commit into
mainfrom
feat/automation-due-trigger
May 28, 2026
Merged

feat(automation): card_due_reached trigger + in-process scheduler#69
Musiker15 merged 1 commit into
mainfrom
feat/automation-due-trigger

Conversation

@Musiker15
Copy link
Copy Markdown
Member

Summary

Completes the v1 automation engine (ADR 0010) — card_due_reached, the last deferred trigger. The fourth and final automation item from the v0.2.0-beta Deferred list.

No new process or systemd unit. Despite ADR 0010 saying "BullMQ", the project's actual worker pattern is an in-process setInterval (see the webhook worker). I followed that: the scheduler starts from instrumentation.ts and runs inside the Next.js server process — single-container deployment story preserved.

How it works

per-minute setInterval (server)
  → find boards with an active card_due_reached rule
  → find cards whose dueAt passed in the last 7 days (indexed query)
  → Redis SET NX automation:due-fired:<cardId> (30d TTL) — claim once
  → publishBoardTick(boardId, { verb: AUTOMATION_DUE_FIRED, targetId: cardId })
        │  (content-free; server never decrypts enc_rule, only reads dueAt)
        ▼
  next online client (SSE) builds the card_due_reached event from its
  current card state → evaluator → executor (normal RBAC'd HTTP path)

Every online client races on the tick; idempotent actions converge and post_comment is SETNX-deduped, so no duplicates.

Changes

  • src/lib/automation/due-scheduler.ts (new): runDueScan() (exported, also drivable by external cron) + startDueScheduler()/stopDueScheduler().
  • instrumentation.ts: starts the scheduler alongside the webhook worker.
  • DSL: card_due_reached added to TRIGGER_TYPES.
  • Evaluator: TriggerEvent variant (columnId + milestoneId), FIELD_ACCESSORS for both, eventToken.
  • board-client: handles the AUTOMATION_DUE_FIRED tick.
  • Rule builder: trigger label + "only when still in column" scope.
  • Ops: AUTOMATION_DUE_WORKER_DISABLED=1; both workers documented in .env.example.

Zero-knowledge / safety notes

  • The server scheduler only reads Card.dueAt (already plaintext + indexed) and the rule's plaintext trigger_type. It never decrypts enc_rule.
  • The board tick is content-free (verb + cardId).
  • Redis claim bounds re-firing: 7-day scan window + 30-day claim TTL means a card fires once; ancient cards drop out of the window.
  • No-cascade invariant preserved: due actions go through the REST API, not the drawer.

Tests

automation-evaluator.test.tscard_due_reached column + milestone scoping, unconditioned match, eventToken. automation-dsl.test.ts — trigger count → 5. 43 automation cases green. Full suite 168 green locally (pnpm typecheck + pnpm lint clean).

The scheduler's runDueScan is integration-level (prisma + redis + pub/sub); consistent with the project's pure-unit convention (the webhook tick has no unit test either), its decision logic is covered via the evaluator + eventToken tests. Manual verification in the test plan.

Test plan

  • CI green
  • Manual: create a rule "When a card's due date is reached → post Overdue!"; set a card's due date to ~1 min in the future; wait; confirm the comment appears once
  • Manual: two browsers on the same board → still exactly one comment (SETNX dedup)
  • Manual: scope a due rule to a column, move the card out before it's due → rule does not fire

🤖 Generated with Claude Code

Completes the v1 automation engine (ADR 0010) — the last deferred
trigger. No new process / systemd unit: the scheduler is an in-process
per-minute setInterval started from instrumentation.ts, mirroring the
webhook worker.

- Scheduler (`due-scheduler.ts`): scans cards whose `dueAt` passed in a
  7-day look-back on boards that have an active card_due_reached rule,
  claims each card once via Redis `SET NX` (30d TTL), publishes a
  content-free `AUTOMATION_DUE_FIRED` board tick. Server only reads the
  already-plaintext `dueAt`; never decrypts enc_rule.
- Client: on the tick, board-client builds the card_due_reached event
  from its current card state and runs the normal evaluator/executor.
  Every online client races; idempotent actions converge and
  post_comment is SETNX-deduped, so duplicates don't happen.
- DSL: card_due_reached added to TRIGGER_TYPES.
- Evaluator: TriggerEvent variant + columnId/milestoneId accessors +
  eventToken.
- Rule builder: trigger label + column-scope condition.
- Ops: `AUTOMATION_DUE_WORKER_DISABLED=1` to drive externally; both
  background workers now documented in .env.example.

Tests: evaluator card_due_reached (column + milestone scoping,
unconditioned) + eventToken; DSL trigger count → 5. 43 automation
cases green. CLAUDE.md + CHANGELOG updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Moritz Kohm <moritz.kohm@gmail.com>
Signed-off-by: Musiker15 <info@musiker15.de>
@Musiker15 Musiker15 merged commit 81ba6cc into main May 28, 2026
8 checks passed
@Musiker15 Musiker15 deleted the feat/automation-due-trigger branch May 28, 2026 15:48
@Musiker15 Musiker15 mentioned this pull request May 28, 2026
5 tasks
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