FS engagement status machine
A financial-statements engagement is one annual or interim engagement for a single client (e.g. ACME's FY2024 statements). It moves through seven states.
What each state means
| State | Meaning | Lock semantics |
|---|---|---|
DRAFT | Engagement created. Header (year-end, reporting standard, currency) editable. No TB yet | TB not loaded; nothing to compute |
TB_UPLOADED | Trial balance ingested (CSV/XLSX). Sign normalized. Awaiting taxonomy mapping | Re-uploading replaces all TB rows; mapping work, statements, and notes invalidated |
TB_MAPPED | Every TB row carries an SFRS taxonomy key (auto-suggested by Claude or manually overridden) | Statements can now be generated |
STATEMENTS_GENERATED | Deterministic Balance Sheet, P&L, SOCE, Cash Flow computed and persisted as the latest version | Disclosure interrogation now meaningful (rule triggers fire on the mapped TB keys) |
NOTES_COMPLETE | Claude generated all notes + Directors Report in a single 16K-token call. Mandatory policies inserted | Engagement is review-ready; staff exports the DOCX from here |
NOTES_ERROR | Notes generation failed (LLM error, JSON parse, schema mismatch). Recoverable | Statements are still valid; retry the notes job |
ARCHIVED | Engagement closed at the end of the audit cycle | Sealed; no further edits |
Why TB upload returns to DRAFT
A trial balance is the foundation. A new TB upload replaces all TB rows and invalidates every downstream artifact (mappings, statements, notes). Returning to DRAFT makes that invalidation visible in the status badge — a reviewer who sees TB_MAPPED → DRAFT knows the upload happened and the rest needs re-running.
Why notes have an explicit error state
The Claude call for notes is one of the longest single requests in the system (16K tokens). When it fails, the staff user must know whether to wait, retry, or fix the input. NOTES_ERROR is the state where the engagement sits while a Senior Accountant looks at the failure record and decides; NOTES_ERROR → STATEMENTS_GENERATED re-enables the retry button.
Where these transitions happen
| Transition | Triggered by |
|---|---|
DRAFT → TB_UPLOADED | parse-trial-balance worker handler completing (POST /ops/financial-statements/engagements/:id/trial-balance) |
TB_UPLOADED → TB_MAPPED | auto-suggest-mapping handler when no rows remain unmapped, or final manual override |
TB_MAPPED → STATEMENTS_GENERATED | generate-statements handler |
STATEMENTS_GENERATED → NOTES_COMPLETE / NOTES_ERROR | generate-notes handler outcome |
NOTES_ERROR → STATEMENTS_GENERATED | Staff-triggered retry of generate-notes |
NOTES_COMPLETE → ARCHIVED | Staff-triggered archive on the engagement detail page |
STATEMENTS_GENERATED → STATEMENTS_GENERATED | Self-loop: re-running generate-statements bumps version_no on FsGeneratedStatement |
Implementation
State transitions live in packages/financial-statements/src/status-transitions.ts and are enforced by canTransitionFsEngagement(from, to). The valid set per current state is queryable via getNextFsEngagementStatuses(from) for UI rendering.
Related
- XBRL filing status machine — the downstream filing that may consume this engagement's output
- Architecture — XBRL & FS integration — full design rationale