Skip to content

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

RolePlays
Platform AdminSets up the client record once — fsConfig, reporting standard, reviewer email
FS PreparerUploads TB, runs mapping auto-suggest, overrides low-confidence rows, generates statements + notes day to day
FS ReviewerReviews the engagement before export; exports the DOCX; routes back to preparer on revision
Senior AccountantSenior judgment on LOW-confidence mappings, sign-flip anomalies, mandatory policy disputes
Client approverOne 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 fsConfig with defaultReportingStandard (SFRS_SE or FULL_SFRS), baseCurrency (typically SGD), fyEndMonth, roundingConvention, plus the entity's uen, registeredAddress, and principalActivities
  • 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 labelFY2024
  • Year-end date2024-12-31 (must agree with fsConfig.fyEndMonth)
  • Reporting standard — defaults from fsConfig.defaultReportingStandard; override for this engagement if needed (e.g. Lumiere passed the small-entity thresholds — switch to FULL_SFRS)
  • Functional currencySGD
  • Has prior yeartrue (Lumiere ships with comparatives)

Submit. Engagement created at DRAFT. URL settles to /dashboard/financial-statements/<engagementId>.

Create an FS engagement

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, OR current_year / prior_year columns. 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 FsTrialBalanceItem rows 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 balanceTB 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:

ConfidenceWhat it meansDefault action
HIGHStrong code + description match against a single taxonomy keyAccept
MEDIUMPlausible mapping with one or two alternativesRead the rationale tooltip; accept or override
LOWWeak match — the AI was not confidentAlways override or confirm explicitly; this is the human-judgment gate
UNCLASSIFIEDNo mapping returnedManually 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 mappingOverride a low-confidence mappingAuto-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 statementsStatement 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:

PriorityRule IDTriggerWhat you provide
mandatoryDISC-001alwaysCompany name, UEN, registered office, principal activities, FY-end
mandatoryDISC-002alwaysReporting standard, functional currency
mandatoryDISC-003alwaysGoing-concern declaration; if no, exit strategy narrative
mandatoryDISC-017alwaysIncome-tax accounting policy (verbatim Section 29 text is auto-inserted into the note; you confirm the rate and any deferred-tax detail)
mandatoryDISC-020alwaysDirectors list, remuneration, auditors / audit exemption
mandatoryDISC-021alwaysEmployee benefits policy (verbatim auto-inserted)
mandatoryDISC-024alwaysEvents after reporting period
mandatoryDISC-025alwaysDividends declared
conditionalDISC-010tb_contains_any(['ppe'])PPE — class, depreciation policy, movement table
conditionalDISC-013tb_contains_any(['trade_receivables'])Trade receivables — aging, impairment
conditionalDISC-014tb_contains_any(['cash'])Cash — composition, restrictions
conditionalDISC-015tb_contains_any(['share_capital'])Share capital — class, movement
conditionalDISC-018tb_contains_any(['revenue'])Revenue — disaggregation
recommendedDISC-019alwaysRelated party transactions
recommendedDISC-023alwaysCommitments & 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 interrogationA 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 FsNote rows 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 notesNotes 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 DOCXExport 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…EffectYour next step
ApproveDecision captured with timestamp; staff notified; engagement remains at NOTES_COMPLETEArchive when the audit cycle closes
Request revisionComment captured; engagement remains at NOTES_COMPLETE; preparer addresses upstreamRe-run the relevant upstream step; re-send

Dispatch the FS portal linkHandle 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.

Archive an FS engagement

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

Cheat sheet — what to do when X

If…Open…Do…
TB upload says "not balanced"Engagement → Trial BalanceFind the unbalanced rows in the source TB; fix at source; re-upload — see TB not balanced
Auto-suggest returns too many UNCLASSIFIEDEngagement → MappingCheck sign normalisation; verify the TB column headers — see Many unclassified
Statement won't balance after generationEngagement → StatementsLook for a sign-flip anomaly in the run output; correct the mapping; regenerate — see Statements not balanced
NOTES_ERROR after generateEngagement → Notes failure recordRead model id + request id; retry if transient; else adjust upstream input — see Notes error
DOCX export refusedEngagement detail pageRead the typed reason (mandatory_policy_mismatch, unbalanced, mandatory_disclosure_missing) — see Export blocked
Client wants a change after ApproveDon't reopen the same engagementCreate a new engagement for the same client + year with a -v2 suffix; document the change reason in the activity log
You took a wrong actionActivity logEvery state change is logged with the actor — see Recover from a mistake

Internal use only — BreezyCorp