Skip to content

Recurring journal entries

Backend domain: app/graphql/recurring_journals/ Migration: add_recurring_journals RBAC Path: RECURRING_JOURNALS

Overview

A RecurringJournal is a template (name + balanced lines + memo template) plus a cadence (frequency, interval, day-of-month/week, optional end-date / occurrence count). The backend tracks nextRunDate, lastRunAt, lastLedgerEntryId, and isActive.

materializeRecurringJournal(id) posts one occurrence on demand. runRecurringJournalsDueToday materializes every active journal whose nextRunDate <= today.

Date arithmetic: monthly cadences honor dayOfMonth with month-length clamping (e.g. day 31 in February → last day of February).

Setup prerequisites

None beyond having GL accounts to debit/credit.

GraphQL surface

Queries

recurringJournals: [RecurringJournal!]!
recurringJournal(recurringJournalId: UUID!): RecurringJournal!

RecurringJournal fields: id, name, memoTemplate, frequency (DAILY | WEEKLY | MONTHLY | QUARTERLY | YEARLY), interval, dayOfMonth, dayOfWeek, startDate, endDate, occurrencesRemaining, nextRunDate, lastRunAt, lastLedgerEntryId, isActive, plus nested lines: [RecurringJournalLine!]!.

RecurringJournalLine: id, recurringJournalId, accountId, debitAmount, creditAmount, memo.

Mutations

createRecurringJournal(recurringJournal: RecurringJournalInput!): RecurringJournal!
pauseRecurringJournal(recurringJournalId: UUID!): RecurringJournal!
resumeRecurringJournal(recurringJournalId: UUID!): RecurringJournal!
skipRecurringJournal(recurringJournalId: UUID!): RecurringJournal!
materializeRecurringJournal(recurringJournalId: UUID!): RecurringJournal!
runRecurringJournalsDueToday: Int!

RecurringJournalInput: name, frequency, interval, startDate, lines: [RecurringJournalLineInput!]!, plus optional memoTemplate, endDate, occurrencesRemaining, dayOfMonth, dayOfWeek.

RecurringJournalLineInput: accountId, debitAmount, creditAmount, memo.

Behaviour to handle in the UI

  • The create form is essentially a manual-journal form plus a cadence section.
  • Validate lines balance on the client too: sum of debits must equal sum of credits (backend will reject otherwise).
  • Show nextRunDate prominently in the list so users know what's coming.
  • pauseRecurringJournal / resumeRecurringJournal should be quick toggles in a row context menu.
  • skipRecurringJournal advances nextRunDate by one cadence without posting — useful for "we already paid this manually this month".
  • materializeRecurringJournal is the manual "Run now" button.

Error states

Backend behaviour When Frontend message
ValueError ("not balanced") Create with unbalanced lines "Lines must balance: debits = credits."
PeriodClosedError (from underlying ledger service) Materializing into a closed period "Cannot post into a closed period. Reopen the period or change the run date."

Suggested UX flow

  1. Recurring journals list with status badge (Active / Paused), nextRunDate, last run, line count.
  2. Create / edit form with cadence picker, line grid, balance check.
  3. Detail page showing the next 6 occurrences (frontend-computed from nextRunDate + cadence) and a history list (query ledgerEntries filtered by recurring_journal_id once that linkage is exposed — see follow-up).

Open follow-ups (not yet implemented)

  • runRecurringJournalsDueToday is exposed as a mutation for manual triggering. There is no scheduled worker task yet that fires it daily — add a render_sdk.Workflows task in worker/tasks.py before relying on auto-posting. For now, surface a "Run due journals" button in the admin area.
  • LedgerEntry does not yet carry a recurring_journal_id source link, so listing past occurrences requires matching last_ledger_entry_id plus walking ledger_entries by memo. Either expose a entriesForRecurringJournal(id) query or store the source linkage.