Write an organization annual report: structure the letter, year highlights, program or business sections, financial summary narrative, and outlook, pull every number only from supplied audited or approved sources, match narrative claims to those numbers, and run a final unsupported-claim review. Use when drafting or revising a yearly report document.
name: annual_report_writing description: "Write an organization annual report: structure the letter, year highlights, program or business sections, financial summary narrative, and outlook, pull every number only from supplied audited or approved sources, match narrative claims to those numbers, and run a final unsupported-claim review. Use when drafting or revising a yearly report document." metadata: { "includeInPrompt": true }
Annual Report Writing
Purpose
Write an annual report that covers one full year of an organization: what it did, what it spent and received, and what it plans next. The report is only as good as its sources. This skill covers assembling the source pack, tracing every claim to that pack, drafting section by section, and reviewing the finished draft for claims the sources do not support.
Scope:
- investor-update-writing is monthly startup investor emails, executive-decision-memos is one-page decision documents, white-paper-writing is evidence and argument papers. This skill owns the yearly report document.
- This skill covers the writing and review of the report narrative. It does not cover preparing audited financial statements, choosing accounting treatments, or meeting filing obligations. Formal requirements for annual reports and financial statements vary by organization type, size, and place, and must be checked for the organization in question. Where this skill talks about the financial section, it means one thing only: the narrative matches the supplied audited or approved figures, with the same periods, units, and labels the source uses.
Workflow
- Assemble the source pack before drafting anything. Do not start prose and hunt for numbers later. Later hunting is how a plausible figure gets written first and justified after.
- Collect, at minimum: the audited financial summary or the approved financial figures for the year; program or business records for the year (outputs, counts, dates, sites, products, as the organization tracks them); the prior year report or prior year figures, if the new report will make any comparison; a list of the year highlights the organization has approved for use; any supplied quotes, with attribution and approval status (step 7); any supplied outlook or plans material approved for publication.
- For each source, record in a source pack list: source name, date or version, the period it covers, who or what body approved it (record the name or role exactly as supplied), and the units and currency it uses.
- Give the pack a version label, such as source pack v1, and note the label in the working files. If a source is replaced mid-draft, bump the label, note what changed, and re-run the checks in step 9 against the new pack.
- If a needed figure is missing from the pack, leave a visible placeholder in the draft, written as [NEED SOURCE: reserves at year end], and record the gap in the source pack list. A placeholder is a work item. An estimate written in the same style as a real figure is a defect.
- Build the claims-to-numbers trace table before writing prose. One row per factual claim the report plans to make. Draft from the table, not from memory or from last year report with the numbers swapped.
- Columns: claim ID, the claim in short form, the number or fact as it will appear, the source name from the source pack list, where in the source it sits (page, table, or line, as supplied), the period it covers, the unit or currency, and a status: traced, derived (see step 6), or cut.
- Cover more than numbers. Dates, site counts written as words, named programs, named partners, awards, firsts, and records are claims too, and each one needs a row and a source.
- Cover qualitative claims as well. A sentence such as "demand grew this year" is a claim. Either tie it to a traced metric and say which one, or rewrite it to state only what the source shows, for example "applications rose from the prior year figure in the program records."
- Worked example rows, from the fixture organization used in the Operating Rules section:
- C-01 | Households served in the year | 12,400 households | Program records | Survey and program summary line | Year 2025 | households | traced
- C-02 | Revenue for the year | $1,850,000 | Financial summary (audited) | Revenue line | Year 2025 | dollars | traced
- C-03 | Reserves at year end | $999,999 | No source in pack | None | Year 2025 | dollars | cut (the claim-check run in the Operating Rules section flags this figure as UNSUPPORTED, and the draft sentence was removed)
- Plan the structure, section by section, before drafting. Assign each section its traced claims by claim ID. A section with no claim IDs assigned has nothing verified to say yet.
- Title and year identification: organization name as supplied, the year or period covered, and the report title. No claims beyond name and period.
- Letter: one short opening letter from a named signer (step 4).
- Year at a glance: the short highlights list, one traced claim per line (step 5).
- Program or business sections: one section per major program, service, or business line (step 5).
- Financial summary narrative: the year money story, written from the supplied financial figures only (step 6).
- Outlook: plans for the next year, written only from supplied and approved plans material (step 8).
- Closing notes: how to reach the organization and any source labels the financial section must carry, copied from supplied wording where the source provides it.
- Length follows the material. Cut a section that has no traced content rather than padding it with general statements about the field the organization works in.
- Write the letter.
- The signer, their name, and their title come from the supplied material, spelled exactly as supplied. If no signer was supplied, leave the signature block as a placeholder and flag it.
- Keep the letter short. One page is a workable target for most reports, because the letter is an introduction, not a second copy of the report.
- Use two or three traced highlights from the trace table and no other numbers. Any number in the letter must also appear, traced, in the body. Check the letter again in step 9, since letters are where an early, unchecked figure often survives.
- Say plainly what kind of year it was, including a difficulty if the supplied material records one. A letter that reports only good news reads as sales copy and makes readers doubt the rest of the report.
- Make no promise in the letter that the outlook section cannot support from supplied plans material.
- Write the year highlights and the program or business sections.
- Year at a glance: one line per highlight, each line a single traced claim with its period and unit attached. Example, from the fixture: "Served 12,400 households in 2025." A highlight with no period forces the reader to guess which year it belongs to.
- Give each program or business section the same shape, so readers can compare sections:
- What it is: one or two sentences on what the program or line does, from supplied descriptions.
- What happened this year: the traced counts and changes for the year, each with period and unit.
- One concrete example: a specific case, site, or piece of work, taken from supplied program notes. An example the notes do not contain is an invented example, even if it sounds typical.
- A difficulty or shortfall, where the supplied records state one, stated in the same plain terms the records use.
- Watch causal wording. Records usually show that an output and an outcome happened in the same year. Write "the program served 340 volunteers and recorded 9,600 volunteer hours" when that is what the records show. Write "the program caused" or "the program led to" only when a supplied source states that finding in those terms, and name that source in the trace table row.
- Name partners, funders, sites, and places only as the sources name them. Do not add a partner logo list, a site list, or an award from memory or from the organization website unless that list is in the source pack.
- Write the financial summary narrative.
- Write narrative that matches the supplied figures. Use the figures the source gives, for the period the source gives, in the currency and units the source gives. Typical supplied items, when the pack contains them: total revenue, revenue by source, total spending, spending by category, the result for the year stated as the source states it (for example surplus or deficit, using the source wording), and reserves or balances at year end.
- Present this section as a summary of the supplied figures, and carry any label the source carries. If the source calls its figures audited, the narrative may say so. If the source calls them draft, approved, or unaudited, use that word instead. Never upgrade the label.
- Rounding: if the narrative rounds a figure for readability, round it in a standard way, keep the full figure in the trace table, and make the rounding visible where it could confuse, for example "$1.85 million (revenue of $1,850,000 in the financial summary)". Do not round in a way that changes a comparison or a total.
- Derived figures: a percentage share or a change from the prior year may be calculated only when every input figure is in the source pack. Write the inputs and the arithmetic into the trace table row, mark the row derived, and have the arithmetic rechecked in step 9. If any input is missing, do not calculate the figure. State the two supplied figures separately instead.
- Comparisons: a claim such as "up 12% from the prior year" needs either the supplied percentage or both year figures in the pack, traced under the derived rule above. A comparison to a year the pack does not cover is cut.
- Totals and shares must agree with each other. If the narrative lists parts and a total, the parts the reader can add must reach the total, or the narrative must say that the list is partial.
- Do not add context figures from outside the pack: no field averages, no sector benchmarks, no cost-per-person figures for other organizations, no matter how useful they would be. Outside context is a separate sourced addition or it stays out.
- Handle quotes under a strict rule.
- Use only quotes that were supplied, with the exact wording supplied, attributed to the named person and role as supplied. Record each quote in the source pack list with its approval status.
- Never write a quote that sounds like what someone probably said. Never combine two supplied remarks into one quotation. Never present a paraphrase inside quotation marks.
- Light cleanup is limited to what the supplied material allows. If no cleanup was approved, print the quote as supplied. Where part of a long supplied quote is omitted, mark the omission with [...] so the gap is visible.
- If a quote contains a number, that number goes in the trace table like any other number and must trace to a source. A quote is not a source for its own figures.
- If a supplied quote has no clear approval for publication in the pack, flag it and leave it out until approval is recorded. Do not assume approval from the fact that the quote was handed over.
- Write the outlook from supplied plans only.
- The outlook states what the organization plans to do next year, taken from the supplied plans material: named programs continuing, planned openings or launches with supplied dates, stated targets that the plans material itself states, and known constraints the material names.
- Keep plans and results in separate wording. Plans use future wording ("plans to open", "aims to"), never wording that lets a planned figure sit next to a result as if both already happened.
- A target stated in the plans material may be printed as a target, labelled as a target, with the period it belongs to. A target the writer invents to make the outlook sound concrete is cut.
- If no plans material was supplied, the outlook is a placeholder flagged for supply, or the report ships without one and the gap is stated in the delivery note. Do not fill the gap with general hopes written in the organization voice.
- Do the plain language pass, then the unsupported-claim review pass. Two separate passes, in that order. Plain language pass:
- One point per paragraph. Split a paragraph that carries two points.
- Short sentences for the financial narrative in particular. A sentence that needs the source open beside it to parse is too dense.
- Define each technical or field term at first use, in the words the supplied material uses where it defines them. Expand each abbreviation at first use.
- Prefer plain words where they keep the meaning: use instead of utilize, help instead of facilitate, about instead of approximately when the figure is already rounded and labelled. Keep a precise term when the plain word would change the meaning, and define it instead.
- Give every percentage its base on first use, for example "8% of total spending", so the reader knows what the share is a share of.
- Headings state what the section covers ("Housing program", "Money in and money out for 2025"), not a slogan. Unsupported-claim review pass:
- Run claim_check.py from the Operating Rules section on a plain text copy of the full draft, with the source pack saved as one text file. Work from its output, not from a read-through.
- Resolve every line marked UNSUPPORTED in one of three ways: correct the figure to the sourced figure, replace the sentence with one the pack supports, or cut the sentence. No fourth option. A figure does not stay because it was in an earlier draft or because it looks right.
- Then check by hand what the script cannot see:
- Numbers written as words (three new sites, half of spending). Each one needs a trace table row.
- Numbers in headings, tables, chart labels, captions, and the letter. Export or copy them into the plain text draft before running the script so the script sees them.
- Context matches. The script matches values only. For each OK line, confirm the draft uses the value for the same metric, period, and unit as the source. A correct revenue figure attached to the wrong year passes the script and fails this check.
- Superlatives and records: largest, first, most, record, best. Each needs a source that states it in those terms, or it is rewritten to the traced fact underneath it.
- Dates, names, titles, and place names against the source pack, letter by letter for names and titles.
- Table arithmetic: re-add every column and row the report prints, and recompute every derived figure from its trace table inputs.
- A second reader for this pass is good practice when one is available, because the writer has already read their own intended meaning into the text once. When no second reader is available, do the pass in a separate sitting from the drafting, working from the script output and the trace table only.
- Assemble and hand over.
- Check the assembled draft once as a reader: section order matches step 3, every placeholder is either resolved or listed in the delivery note, the letter numbers match the body numbers, and the financial labels match the source labels.
- Hand over the working record with the draft: the source pack list with its version label, the trace table with statuses, and the final claim-check output. A draft handed over without its trace table cannot be checked by the next reader and will be re-checked from zero.
- In the delivery note, state plainly: which sources were used, any placeholders or cut claims and why, and any supplied material that arrived too late or unapproved and was left out.
Output Contract
Deliver an annual report draft with these sections, in this order:
- Title block: organization name as supplied, report title, and the year or period covered.
- Letter: signed with the supplied name and title, using only numbers that appear traced in the body.
- Year at a glance: the highlights list, one traced claim per line, each with its period and unit.
- Program or business sections: one per major program or line, each in the step 5 shape (what it is, what happened this year, one supplied example, a stated difficulty where the records state one).
- Financial summary narrative: revenue, spending, result for the year, and balances as supplied, with source labels carried exactly, rounding shown where used, and derived figures marked in the trace table.
- Outlook: supplied plans only, in future wording, with targets labelled as targets.
- Closing notes: contact wording as supplied and any source labels the financial section must carry.
Plus the working record, delivered with the draft and not printed in the report:
- Source pack list: each source with name, date or version, period covered, approval as supplied, units and currency, and the pack version label.
- Trace table: one row per claim with claim ID, claim, figure or fact, source, source location, period, unit, and status. On the worked fixture in the Operating Rules section, the claim-check run reports: total numbers 14, supported 13, unsupported 1, and the single unsupported figure is the planted $999,999 reserves figure, which is cut.
- Final claim-check output: the claim_check.py output for the final draft, showing unsupported: 0 for a draft that is ready to hand over.
A sample trace table row:
- Claim ID: C-02
- Claim: Revenue for the year
- Figure as printed: $1,850,000
- Source: Financial summary (audited)
- Location in source: Revenue line
- Period: Year 2025
- Unit: dollars
- Status: traced
A sample highlights line:
- Served 12,400 households in 2025, and completed 47 community workshops across 3 new service sites.
Operating Rules
- HARD RULE, no exceptions: never invent figures, percentages, or quotes. Every number in the report traces to a source the writer was given: the source pack from Workflow step 1, recorded in the trace table from Workflow step 2. A figure with no trace is removed or replaced with a sourced figure. It is never estimated, rounded into existence, borrowed from another organization, or carried over from a prior year report as if it covered this year.
- Every claim traces, not only every number. Dates, counts written as words, names, titles, partnerships, awards, firsts, and records each need a trace table row and a source in the pack.
- Quotes are supplied or they do not appear. Exact supplied wording, named attribution as supplied, approval status recorded in the source pack list. No reconstructed, combined, or paraphrased text inside quotation marks.
- Labels never get upgraded. Audited, approved, draft, and unaudited are the source words. The narrative uses the word its source uses, for the figures that source covers.
- Derived figures carry their work. A calculated share or change is allowed only when all inputs are in the pack, the arithmetic is written in the trace table, the row is marked derived, and the arithmetic is rechecked in the review pass. One missing input ends the calculation.
- Formal requirements vary and are checked outside this skill. This skill makes no claim about what any law, regulator, funder, or accounting standard requires for a given organization. The writer follows the supplied material and flags format or filing questions for the organization to settle through its own channels, rather than settling them from general knowledge.
- Superlatives need a source that says them. Largest, first, record, and most are claims about everything outside the report as well as inside it. Without a supplied source stating them, rewrite to the traced fact.
- Placeholders stay visible. A missing figure is a [NEED SOURCE: ...] placeholder listed in the delivery note. Never fill a gap with a smooth sentence that hides the gap, and never let a placeholder reach the published report unlisted.
- Use the claim-check script below for the numeric review. Save it as claim_check.py, save a plain text copy of the draft as draft.txt and the text of the source pack as sources.txt, then run it. Resolve its UNSUPPORTED lines under Workflow step 9 and quote counts from its output in the working record, not from a hand count. The script exits with code 1 when any unsupported number remains and code 0 when all numbers found are supported, so a ready draft is one where the run exits 0.
- Context is checked by hand. The script matches numeric values only. Same value, wrong metric, wrong year, or wrong unit is still an error, and only the manual checks in Workflow step 9 catch it.
Claim check (claim_check.py)
Input format: two plain text files. draft.txt holds the draft text, one line per line of the report copy (include headings, tables, captions, and the letter as plain lines, so their numbers are checked too). sources.txt holds the text of the supplied source pack, pasted or typed as plain text.
Example draft.txt (the fixture used for the expected output below). Note the reserves figure on line 8, which appears in no source:
text
Riverside Community Trust Annual Report 2025
In 2025 we served 12,400 households across the region.
Revenue for the year was $1,850,000, up 12% from the prior year.
Our volunteer team grew to 340 active volunteers who gave 9,600 hours.
Administrative costs were 8% of total spending.
We opened 3 new service sites and completed 47 community workshops.
A grant of $250,000 funded the housing program.
Our reserves reached $999,999 at year end.
Satisfaction reached 91% in the annual survey of 620 respondents.
Example sources.txt (the text of the supplied source pack for that draft):
text
Approved source pack for the 2025 annual report
Financial summary (audited): revenue $1,850,000, growth 12%, administrative share 8%, housing grant $250,000
Program records: households served 12,400, active volunteers 340, volunteer hours 9,600, new sites 3, workshops 47
Survey summary: respondents 620, satisfaction 91%
Run: python3 claim_check.py draft.txt sources.txt
Expected output for those files (the run exits with code 1, because one figure is unsupported):
text
line 1 | 2025 | OK | Riverside Community Trust Annual Report 2025
line 2 | 2025 | OK | In 2025 we served 12,400 households across the region.
line 2 | 12,400 | OK | In 2025 we served 12,400 households across the region.
line 3 | $1,850,000 | OK | Revenue for the year was $1,850,000, up 12% from the prior year.
line 3 | 12% | OK | Revenue for the year was $1,850,000, up 12% from the prior year.
line 4 | 340 | OK | Our volunteer team grew to 340 active volunteers who gave 9,600 hours.
line 4 | 9,600 | OK | Our volunteer team grew to 340 active volunteers who gave 9,600 hours.
line 5 | 8% | OK | Administrative costs were 8% of total spending.
line 6 | 3 | OK | We opened 3 new service sites and completed 47 community workshops.
line 6 | 47 | OK | We opened 3 new service sites and completed 47 community workshops.
line 7 | $250,000 | OK | A grant of $250,000 funded the housing program.
line 8 | $999,999 | UNSUPPORTED | Our reserves reached $999,999 at year end.
line 9 | 91% | OK | Satisfaction reached 91% in the annual survey of 620 respondents.
line 9 | 620 | OK | Satisfaction reached 91% in the annual survey of 620 respondents.
total numbers: 14
supported: 13
unsupported: 1
claim_check.py (Python 3, standard library only). Save this code as claim_check.py:
python
#!/usr/bin/env python3
"""List numbers in a draft and flag any not found in a source file. Stdlib only.
Extracts currency amounts, percentages, and plain numbers (integers and
decimals, with optional thousands commas) from the draft, in order, with
line numbers and the draft line as context. A number counts as supported
when the same numeric value appears anywhere in the source file, under
the same extraction. Values are compared after removing $, %, and commas,
so 12,400 in the draft matches 12400 in the source. This is a tracing aid
for manual review, not a proof: a matching value in the wrong context is
still wrong, and the reviewer checks context in the trace table.
Usage: python3 claim_check.py draft.txt sources.txt
"""
import re, sys
NUM = r"(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?"
PATTERN = re.compile(r"\$" + NUM + r"|" + NUM + r"%|" + NUM)
def normalize(raw):
value = raw.replace("$", "").replace("%", "").replace(",", "").strip()
try:
number = float(value)
except ValueError:
return value
if number.is_integer():
return str(int(number))
return ("%f" % number).rstrip("0").rstrip(".")
def extract(text):
found = []
for match in PATTERN.finditer(text):
found.append((match.group(0), normalize(match.group(0))))
return found
def main():
if len(sys.argv) != 3:
print("usage: python3 claim_check.py draft.txt sources.txt", file=sys.stderr)
return 2
try:
with open(sys.argv[1], encoding="utf-8") as f:
draft_lines = f.read().splitlines()
with open(sys.argv[2], encoding="utf-8") as f:
source_text = f.read()
except OSError as e:
print(f"error: {e}", file=sys.stderr)
return 2
source_values = {norm for _raw, norm in extract(source_text)}
total = 0
unsupported = 0
for line_no, line in enumerate(draft_lines, start=1):
for raw, norm in extract(line):
total += 1
if norm in source_values:
status = "OK"
else:
status = "UNSUPPORTED"
unsupported += 1
print(f"line {line_no} | {raw} | {status} | {line.strip()}")
print(f"total numbers: {total}")
print(f"supported: {total - unsupported}")
print(f"unsupported: {unsupported}")
return 1 if unsupported else 0
if __name__ == "__main__":
raise SystemExit(main())
No ratings yet — be the first to rate it.