Quick start — trial balance to publication-ready DOCX
A narrative walkthrough of the financial-statements flow from the staff side. By the end you'll have moved one client through one full engagement — trial balance ingested, every account mapped to an SFRS taxonomy key, Balance Sheet / P&L / SOCE / Cash Flow generated and balanced, disclosure interrogation completed, notes + Directors Report generated, and a publication-ready DOCX exported. Every section links into the matching how-to runbook for click-by-click detail and into the troubleshooting page that covers the most common stumble at that step.
The story below follows one client end to end. We'll use Lumiere Trading Pte Ltd — a hypothetical Singapore-incorporated wholesale-trade SME on your firm's portfolio with a 31 December year-end, SGD functional currency, and full prior-year comparatives. Anchor the steps on whichever client you're actually running; the surfaces are identical.
NOTE
If you're new to the dashboard, start with the dashboard tour first — it names every sidebar surface this page sends you to. If you're new to the codebase, start with the manual testing playbook which walks every layer (schema, constitution, gate, FSM) in the current Phase 0 / Phase 1-foundation state.
Cast of characters
| Role | Plays |
|---|---|
| Platform Admin | Sets up the client record once — fsConfig, reporting standard, reviewer email |
| FS Preparer | Uploads TB, runs mapping auto-suggest, overrides low-confidence rows, generates statements + notes day to day |
| FS Reviewer | Reviews the engagement before export; exports the DOCX; routes back to preparer on revision |
| Senior Accountant | Senior judgment on LOW-confidence mappings, sign-flip anomalies, mandatory policy disputes |
| Client approver | One contact at Lumiere who reviews the DOCX preview via the magic link and signs off |
0. Confirm the client is configured
A financial-statements client needs:
- Enabled products including Financial Statements
- An
fsConfigwithdefaultReportingStandard(SFRS_SEorFULL_SFRS),baseCurrency(typicallySGD),fyEndMonth,roundingConvention, plus the entity'suen,registeredAddress, andprincipalActivities - One submitter contact (rare for FS — most engagements run staff-side) and one approver contact for the optional portal review
Open Clients in the staff dashboard sidebar, click into Lumiere. The setup health panel goes green when the essentials are in place. Anything missing refuses the engagement-creation surface with a specific error.
→ Create an FS engagement for the cold-start path → Configure fsConfig for a client
1. Create the engagement
Open Lumiere; click New financial statements engagement. Fill in:
- Financial year label —
FY2024 - Year-end date —
2024-12-31(must agree withfsConfig.fyEndMonth) - Reporting standard — defaults from
fsConfig.defaultReportingStandard; override for this engagement if needed (e.g. Lumiere passed the small-entity thresholds — switch toFULL_SFRS) - Functional currency —
SGD - Has prior year —
true(Lumiere ships with comparatives)
Submit. Engagement created at DRAFT. URL settles to /dashboard/financial-statements/<engagementId>.
2. Upload the trial balance
Open the Trial Balance tab on the engagement. Click Upload Trial Balance and drop in Lumiere's TB — CSV or XLSX, max 10 MB.
The parse-trial-balance handler runs:
- Smart column detection — recognises
account_code,description,debit,credit, ORcurrent_year/prior_yearcolumns. Case-insensitive, underscore/space-agnostic. - Sign normalisation — debit-positive in storage. Credits land as negative
currentYearBalance. - Year detection — finds the closest 4-digit prior-year column; sets
hasPriorYear = true. - Append-replace — all prior
FsTrialBalanceItemrows for this engagement are dropped.
The UI shows a preview table with row count and a balance check (Σ currentYearBalance must be zero ±1.0 SGD). Lumiere ships balanced — green badge.
Engagement transitions DRAFT → TB_UPLOADED.
NOTE
Re-uploading a TB returns the engagement to DRAFT and invalidates every downstream artifact (mappings, statements, notes). The status badge will visibly regress — that's by design so a reviewer sees the invalidation.
→ Upload a trial balance → TB upload says "not balanced" when the upload preview shows a non-zero sum → TB upload parses no rows when the smart-column detection comes up empty
3. Map the trial balance to SFRS taxonomy
Open the Mapping tab. The table shows every TB row with an empty taxonomyKey column.
Click Auto Suggest. The handler enqueues auto-suggest-mapping. Claude Sonnet receives every TB row + the full flattened SFRS taxonomy and returns per-row {taxonomyKey, taxonomyLabel, confidence, rationale}. The table refreshes with confidence badges:
| Confidence | What it means | Default action |
|---|---|---|
| HIGH | Strong code + description match against a single taxonomy key | Accept |
| MEDIUM | Plausible mapping with one or two alternatives | Read the rationale tooltip; accept or override |
| LOW | Weak match — the AI was not confident | Always override or confirm explicitly; this is the human-judgment gate |
| UNCLASSIFIED | No mapping returned | Manually pick from the dropdown |
For Lumiere a typical run on a 60-row TB:
- 45 HIGH (cash, trade receivables, share capital, revenue, COGS — the obvious wins)
- 11 MEDIUM (the "Other Income" cluster, the accumulated-depreciation contra)
- 4 LOW or UNCLASSIFIED (a CSR donations line, a one-off legal-settlement provision, an FX-revaluation line, and a director-loan receivable)
Walk every LOW / UNCLASSIFIED row. Open the dropdown; pick the right key. Manual overrides set isOverride = true and mappingConfidence = MANUAL. The activity log captures who overrode and what.
When every row has a taxonomyKey, the engagement transitions TB_UPLOADED → TB_MAPPED.
→ Run auto-suggest mapping → Override a low-confidence mapping → Auto-suggest returned UNCLASSIFIED for too many rows — usually a TB sign-normalisation surprise → Mapping dropdown doesn't show the key I want — the key isn't in the constitution; check the SFRS taxonomy reference
4. Generate the statements
Open the Statements tab; click Generate. The generate-statements handler runs synchronously — no AI involved at this step, every figure is a pure function of the mapped TB.
The four statements render in tabs:
- Statement of Financial Position — Non-current assets → Current assets → Total assets ‖ Equity → Non-current liabilities → Current liabilities → Total equity & liabilities. Balance check ±1.0 SGD.
- Statement of Profit or Loss and OCI — Revenue (sign-flipped from credit) − COGS = Gross Profit; + Other Income − Selling/Admin/Finance/Other = PBT; − Tax = PAT.
- Statement of Changes in Equity — opening equity + PAT + share movements = closing equity. Columns with no movement are auto-omitted.
- Statement of Cash Flows (indirect) — Operating = PBT + D&A + working capital changes − tax paid; Investing = PPE / ROU / intangibles / investments; Financing = share / loan / lease movements. Reconciles to opening + closing cash.
For Lumiere the run lands isBalanced = true, profit after tax SGD 412,300 (PY: SGD 318,400), with two anomalies: a negative_working_capital_minor advisory on a SGD 4,200 dip below zero (within tolerance), and a sign_flip info on one mapping (interest income mapped to revenue should have been other_income — fix in the mapping tab and regenerate).
Engagement transitions TB_MAPPED → STATEMENTS_GENERATED. Re-running bumps versionNo and keeps the latest as the active version.
→ Generate statements → Statement says "not balanced" — usually a sign-flip on one mapping → Anomaly: negative working capital when the warning appears legitimately
5. Disclosure interrogation
Open Interrogation. The disclosure rule engine evaluates evaluateDisclosureGaps(presentKeys) live against Lumiere's mapped TB keys and surfaces the triggered rules sorted by priority.
For Lumiere the accordion shows:
| Priority | Rule ID | Trigger | What you provide |
|---|---|---|---|
mandatory | DISC-001 | always | Company name, UEN, registered office, principal activities, FY-end |
mandatory | DISC-002 | always | Reporting standard, functional currency |
mandatory | DISC-003 | always | Going-concern declaration; if no, exit strategy narrative |
mandatory | DISC-017 | always | Income-tax accounting policy (verbatim Section 29 text is auto-inserted into the note; you confirm the rate and any deferred-tax detail) |
mandatory | DISC-020 | always | Directors list, remuneration, auditors / audit exemption |
mandatory | DISC-021 | always | Employee benefits policy (verbatim auto-inserted) |
mandatory | DISC-024 | always | Events after reporting period |
mandatory | DISC-025 | always | Dividends declared |
conditional | DISC-010 | tb_contains_any(['ppe']) | PPE — class, depreciation policy, movement table |
conditional | DISC-013 | tb_contains_any(['trade_receivables']) | Trade receivables — aging, impairment |
conditional | DISC-014 | tb_contains_any(['cash']) | Cash — composition, restrictions |
conditional | DISC-015 | tb_contains_any(['share_capital']) | Share capital — class, movement |
conditional | DISC-018 | tb_contains_any(['revenue']) | Revenue — disaggregation |
recommended | DISC-019 | always | Related party transactions |
recommended | DISC-023 | always | Commitments & contingencies |
Walk each rule; answer every required prompt. The UI hides conditional prompts whose visible_when predicate is false (e.g. DISC-003 deferred-tax detail only appears if the company answered "no" to going-concern). Click Save per rule; the handler upserts FsDisclosureResponse rows keyed (engagementId, ruleId, promptId).
The progress bar tracks complete / triggered. You can move on to the next step the moment every mandatory rule has every required prompt answered — conditional rules can be deferred to round 2 if the client owes you the data.
→ Complete the disclosure interrogation → A rule I expected isn't triggered when the conditional clause doesn't match what you see in the TB
6. Generate notes + Directors Report
Open Notes; click Generate notes. The generate-notes handler enqueues as a background job (the UI shows a polling indicator).
Background:
- Loads disclosure responses, generated statements, engagement + client metadata, and the verbatim mandatory policies from
packages/financial-statements/constitution/mandatory_policies/. - Builds one consolidated 16K-token Claude Sonnet prompt — single call by design, both to keep cost predictable and to give the LLM all the context at once so cross-note consistency is structural.
- Receives
{notes: [{note_key, title, blocks}], directors_report: {blocks}}. - Replaces every
{type: "financial_results_placeholder"}block with a computed financial-results table (deterministic post-processing). - Upserts
FsNoterows keyed(engagementId, noteKey).
On success: engagement transitions STATEMENTS_GENERATED → NOTES_COMPLETE.
On failure (timeout, JSON parse, schema mismatch): engagement transitions to NOTES_ERROR. Statements remain valid; only the notes call failed. Open the failure record on the Notes tab — it shows the model id, request id, and prompt fingerprint. Decide:
- Retry — if the error looks transient (rate limit, fleeting timeout). One click sends the same prompt again.
- Adjust upstream — if a disclosure answer doesn't parse, or a TB sign flip is feeding contradictory inputs to the LLM, fix at source then retry.
For Lumiere the run takes ~30 seconds end-to-end and produces:
- Directors Report — Companies Act Section 201 form with the injected financial-results table at the exact placeholder position.
- Note 1 — General Information — name, UEN, registered office, principal activities.
- Note 2 — Basis of Preparation — reporting standard, functional currency, rounding.
- Note 3 — Significant Accounting Policies — Section 29 verbatim text + Employee Benefits + Provisions.
- Note 4 — PPE — opening + additions + depreciation + closing, by class.
- Note 5 — Trade Receivables — aging analysis.
- Note 6 — Revenue — disaggregation by service line.
- Note 7 — Related Party Transactions.
- … etc.
→ Generate notes → Notes generation went to NOTES_ERROR — what to read in the failure record → Mandatory policy text doesn't appear in note 3 — the renderer didn't pick up the constitution file
7. Export the DOCX
FS_REVIEWER, SENIOR_ACCOUNTANT, and PLATFORM_ADMIN can export. FS_PREPARER cannot — by design.
Open the engagement; click Export Word. The handler enqueues export-statements-docx. The renderer reads the latest FsGeneratedStatement and every FsNote, then produces:
- Cover page — company name, title, year-end date, UEN
- Running header — company name left, year-end right, thin bottom border
- Page footer — centred 8 pt page numbers
- Directors Report — including the injected Financial Results table
- SOFP — 4-column (Description / Note / CY / PY)
- SPLOCI — 2-column
- SOCE — zero-movement columns auto-omitted
- SOCF — Operating → Investing → Financing
- Notes — auto-numbered from 1; paragraphs, subheadings, right-aligned amount tables
Renderer asserts: statement is balanced (or banner-warns on the cover); every mandatory rule answered (or refuses); mandatory policy verbatim text matches the constitution before write (or refuses).
The DOCX lands in S3 at financial-statements/<clientId>/<engagementId>/<filename>.docx and a signed URL is returned. Multiple exports are allowed; each is audit-logged.
→ Export the DOCX → Export refused: "mandatory policy mismatch" — a note was edited away from the constitution text → Export refused: "engagement not balanced" — regenerate after fixing the mapping
8. (Optional) Client portal sign-off
If Lumiere wants to sign off before publication, click Send to client for review on the engagement. The handler creates a PortalInvitation with resourceType = FS_ENGAGEMENT and 30-day expiry; the approver gets an email:
- Subject: Financial statements ready for review: Lumiere Trading Pte Ltd FY2024
- Body: A short note, then a single Open the review portal button.
The portal renders the statement preview + DOCX download. Two CTAs: Approve and Request revision.
| If they pick… | Effect | Your next step |
|---|---|---|
| Approve | Decision captured with timestamp; staff notified; engagement remains at NOTES_COMPLETE | Archive when the audit cycle closes |
| Request revision | Comment captured; engagement remains at NOTES_COMPLETE; preparer addresses upstream | Re-run the relevant upstream step; re-send |
→ Dispatch the FS portal link → Handle a client revision request
9. Archive
When the audit cycle closes, click Archive. The engagement transitions NOTES_COMPLETE → ARCHIVED and becomes read-only. The retention policy (default 7 years from year-end, configurable per client jurisdiction) applies.
What you've just done
You've shipped Lumiere through one complete FS engagement: TB ingested and mapped to SFRS, statements generated and balanced, every triggered disclosure rule answered, notes + Directors Report authored by Claude with verbatim mandatory policies inserted unaltered, DOCX exported to publication quality, optional client sign-off captured, engagement archived. Every action is in the Activity log, and per-engagement audit is on the engagement detail page.
Where to go next
- Run a real engagement — open Financial Statements in the sidebar, pick the next client whose year-end is approaching, and follow this same arc.
- File XBRL for the same period — Quick start — XBRL filing. Phase 3 will let you hand off from the FS engagement directly; until then it's an independent flow.
- Understand the status machine — FS engagement states.
- Read the SOP this flow implements — Financial Statements SOP.
- Walk the architecture — XBRL & FS integration plan.
Cheat sheet — what to do when X
| If… | Open… | Do… |
|---|---|---|
| TB upload says "not balanced" | Engagement → Trial Balance | Find the unbalanced rows in the source TB; fix at source; re-upload — see TB not balanced |
| Auto-suggest returns too many UNCLASSIFIED | Engagement → Mapping | Check sign normalisation; verify the TB column headers — see Many unclassified |
| Statement won't balance after generation | Engagement → Statements | Look for a sign-flip anomaly in the run output; correct the mapping; regenerate — see Statements not balanced |
NOTES_ERROR after generate | Engagement → Notes failure record | Read model id + request id; retry if transient; else adjust upstream input — see Notes error |
| DOCX export refused | Engagement detail page | Read the typed reason (mandatory_policy_mismatch, unbalanced, mandatory_disclosure_missing) — see Export blocked |
| Client wants a change after Approve | Don't reopen the same engagement | Create a new engagement for the same client + year with a -v2 suffix; document the change reason in the activity log |
| You took a wrong action | Activity log | Every state change is logged with the actor — see Recover from a mistake |