# Business Narrative Analyst

> Stage brief: write the story and map it to drivers.

- Skill: `lyndonkl/business-narrative-analyst` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lyndonkl/business-narrative-analyst`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lyndonkl/business-narrative-analyst/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: lyndonkl (https://skillmd.com/u/lyndonkl)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lyndonkl/business-narrative-analyst

---

# Business narrative analyst (stage brief)

This is the brief the valuation orchestrator sends to its teammate Bot as a job for the narrative
stage. The job message carries the run's absolute paths and the mandate currency and
valuation date; the Bot resolves its own skills root. It writes the business
story and converts it into the drivers the model consumes; it does not value the company,
clean statements or estimate a cost of capital.

## When to Use

- Loaded by the orchestrator after classification and before any forecast, in parallel
  with the statement-repair stage, whenever a valuation needs defensible growth, margin,
  reinvestment and survival assumptions.
- Loaded when earnings are negative or at a trough and growth has to be built from revenue
  and a target margin.
- Loaded again when a critic finding targets a driver, a claim grade or a counter-narrative.
- Not for direct use. If you are reading this outside a team run, load
  `narrative-to-numbers` instead.

## Role

You own the story and its drivers. You write `narrative.md`, the prose case for what this
business becomes, and `drivers.json`, the payload that turns that case into numbers. Every
claim in the story moves exactly one driver, and every driver carries one sentence of story.
Those two counts are the whole point of the stage.

You do not value the company. The forecast, the discounted cash flow, the bridge and the
value per share belong to the intrinsic-valuation stage. You do not clean statements, and
you do not estimate a cost of capital. What you produce is the set of assumptions those
stages would otherwise have to invent, with the argument attached to each one.

The failure this stage exists to prevent is a valuation that starts at a driver template and
grows a story afterwards. That produces a rationalisation. It is internally consistent with
itself and with nothing else, and no downstream check can detect it.

## Inputs

The orchestrator supplies an absolute path for every input and output. You never assume a
directory layout, and you never read a file you were not handed.

| Input | What you take from it | If missing or malformed |
|---|---|---|
| `mandate.json` | valuation currency, valuation date, mode | stop; return `blocked` naming the field |
| `classification.json` | `life_cycle_stage`, `earnings_status`, `sector_type`, `intangible_intensity`, `geography`, `primary_path`, `overlays`, `constraints` | stop; return `blocked`. No narrative work runs before classification |
| `raw-financials.json` | revenue, operating income, invested-capital components, share count, debt, cash, loss carryforwards | work from whatever is present, list every gap in the return |
| `market-data.json` | current price, government bond rate in the mandate currency, index and sector data | if the price is absent, say so and continue; the story does not need it |
| `gaps.json` | what the collector could not find, and its named fallback | treat each gap as a constraint on how precise a driver can be |
| `cleaned-financials.json` | adjusted EBIT, invested capital, reinvestment rate, ROIC, tax path | usually absent, since this stage runs beside the statement work. See the base-year rule below |

**The base-year rule.** Base-year fields are the seam between this stage and the statement
work. When the orchestrator hands you `cleaned-financials.json`, take `base_revenue`,
`base_ebit` and `base_invested_capital` from it and set `base_year_basis` to `cleaned`. When
it does not, take them from `raw-financials.json`, set `base_year_basis` to
`raw-provisional`, and name those three fields in your return as figures the
intrinsic-valuation stage must overwrite. A base year with research and development still
expensed understates both operating income and invested capital, so every margin and return
you benchmark against the industry is wrong in the same direction.

**Rates you do not own.** The cost of capital belongs to the cost-of-capital stage, and it
usually runs after you. Set the *shape* of the rate path from the story — where it starts,
where it lands, how long it takes — and mark the levels `provisional` when
`cost-of-capital.json` was not supplied. Same for the riskfree rate that caps terminal
growth: use the cost-of-capital artifact when you have it, otherwise the ten-year government
bond rate in the mandate currency from `market-data.json`, and record which you used.

## Preconditions

Check all of these before any work. If one fails, stop and return `blocked` naming exactly
what you need. Do not guess and proceed.

1. `classification.json` exists and carries a `primary_path` and a compiled constraint list.
2. The mandate fixes a currency and a valuation date, and you can state both.
3. There is a base-year revenue figure, or the company is pre-revenue and the classification
   says so. A pre-revenue company needs a first-revenue year, which is a story claim you
   write down rather than a number you find.
4. There is at least one third-party estimate of the market this company sells into, with a
   source and a date. Without it the growth lever has no anchor. Return `needs_input` with
   the specific question and two or three candidate market definitions.
5. The constraint set does not contain `no-intrinsic-valuation`. When it does, write
   `narrative.md` alone, skip `drivers.json`, and return `blocked` explaining that the asset
   can be priced but not valued and that the pricing route applies instead.

## Process

`<skills>` is the absolute path of the corporate-finance skills directory; the orchestrator
substitutes the real path into this brief before delegating. If the literal token survives,
call `skill_view("dcf-valuation-engine")` and take the parent directory of the `skill_dir`
field in the result; never guess a path. Scripts run through `terminal`.

Call `skill_view("narrative-to-numbers")` before you start; it carries the method. Its
reference files hold the detail behind each step:
`skill_view("narrative-to-numbers", file_path="references/narrative-tests.md")` for the
screens, `driver-mapping.md` for the lever menus and terminal defaults, `failure-gallery.md`
for the three pathologies, and `worked-examples.md`, all under the same `references`
directory. Reach for `company-classification-routing` with `skill_view` if a constraint's
scope is unclear, and for `option-valuation-toolkit` only to hand the option layer to the
real-options stage, never to value it here.

**1. Run the bias audit, before you open the price.** Name who the valuation is for, what
answer that party wants, and whether you have already seen the market price. If you have,
treat it as an anchor to argue against. Record the audit in `narrative.md`.

**2. Read the constraints and state which bind this stage.** Quote each one with its reason.
The ones that reach the drivers:

| Constraint | What it does to your payload |
|---|---|
| `no-standard-growth-model` | growth comes from a revenue path and a target margin, never an earnings growth rate. See step 7 |
| `require-failure-probability` | the `failure` block is mandatory, with a stated proceeds basis |
| `no-perpetual-growth-above-riskfree` | `terminal.growth_rate` is capped at the riskfree rate in the mandate currency |
| `require-normalized-earnings` | the base year is a cycle-normalized figure, not the reported one; say which window |
| `no-fcff-valuation` | this payload does not apply. Write the narrative and the claim ledger, mark `payload_applies` false, and name the special-situations route |
| `require-total-beta`, `require-illiquidity-discount` | they land on other stages; note them in the return so nobody assumes you applied them |

**3. Survey the landscape.** Map the business as a loop in about six boxes: suppliers,
customers, who sets price, what slice the firm keeps, what it must invest in to grow, what it
spends to get there. Then benchmark three things against the industry and against the largest
incumbents: revenue growth, pre-tax operating margin, and sales-to-capital or return on
capital. For a company claiming it will become the largest player, look at what the largest
players actually earn. Use `web_search` and `web_extract` for market size, peer economics and
competitor filings, and record the URL and retrieval date for each. Industry tables live in
`<skills>/cost-of-capital-toolkit/scripts/data/` with an `as_of` field
(`python3 <skills>/cost-of-capital-toolkit/scripts/reference_data.py lookup` reads a row);
record that vintage, and refresh from the source when it is more than a year stale.

**4. Write the narrative in prose, with the payload closed.** Keep it simple, keep it
focused, stay grounded. If it does not fit in a paragraph, it is not a narrative yet. Give it
a title that states the bet. A story that rules nothing out sets no driver; rewrite until a
reader could show one of its claims false.

**5. Break the prose into discrete claims and grade each one.** One market, one capability,
one margin path per claim.

| Grade | Definition | Where it goes | Promotion trigger |
|---|---|---|---|
| Probable | expected, with evidence in product success or financial results | base-year numbers and expected cash flows | — |
| Plausible | a reasoned argument, no tangible evidence yet | a higher growth rate inside the model | product success, financial results |
| Possible | the probability cannot be assessed at all | the option layer, added once on top | market-potential evidence, product testing |

Possible does not mean unlikely. It means unassessable, which is why it gets a different
tool. Route each claim once. A market counted in revenues may not also be counted as option
value. Write the promotion trigger down now, because the feedback loop has nothing to watch
otherwise.

**6. Run the screens.** The impossible is never allowed: perpetual growth above the riskfree
rate, implied revenue above the total market, a margin above 100%, depreciation above capital
spending in perpetuity. The implausible needs an extraordinary argument: growth without
reinvestment, rising profits with no competitive response, high returns in a business with no
risk. The improbable is the dangerous class, where each assumption is fine alone and the
combination is not: high growth with low risk, high growth with low reinvestment, low risk
with high reinvestment. Score the three runaway ingredients as well — a charismatic narrator,
a disliked incumbent being disrupted, a claimed societal benefit. Two or three of them means
you write down the questions you have been avoiding and ask them. When the company is one of
many chasing a single market, run the aggregation test. Impute each competitor's breakeven
revenue in a common future year, weight it by that competitor's share of revenue from the
market, and sum. Compare the total against an independent market forecast.

**7. Map every claim to exactly one payload field.** The value chain is fixed: total market
times market share gives revenue, revenue times margin gives operating income, less taxes and
less reinvestment gives the cash flow.

| The claim is about | Payload field |
|---|---|
| Market definition, market size, market growth | `base_revenue` plus `revenue_growth` |
| Competitive position, attainable share, the slice kept | `revenue_growth` |
| Pricing power, cost structure, scale economies | `operating_margin` |
| How fast scale arrives | `operating_margin.converge_by` |
| Capital intensity | `sales_to_capital` |
| Tax domicile, tax migration, accumulated losses | `tax_rate`, `net_operating_loss_carryforward` |
| Business maturity, operating risk, country exposure | `cost_of_capital`, `terminal.cost_of_capital` |
| How long the moat holds competition off | `forecast_years`, each `converge_by` |
| Whether excess returns survive forever | `terminal.return_on_capital` |
| The ceiling on perpetual growth | `terminal.growth_rate` |
| Whether the business can fail outright | `failure.probability`, `failure.proceeds_basis`, `failure.proceeds_percent` |
| Debt, cash, minorities, options, share count | `bridge.*` |
| A market you cannot assign a probability to | none — the option layer, valued elsewhere and added once |

Set the growth lever from an end-state revenue level, never from a rate you like the look of.
Compute it through `terminal`, do not estimate it in prose:

```bash
python3 -c "print((TARGET_REVENUE_N / BASE_REVENUE) ** (1.0/N) - 1)"
```

Record that expression, its inputs and its result in `revenue_build`, and keep the market
size, its growth rate, the attained share and the revenue slice there too. Those are the
numbers a reader will argue with; the growth rate is only what the engine eats.

Give every lever a reference class and a range. A target margin picked against a named
industry percentile is a claim you can defend. A margin picked from nowhere is a number
nobody can argue with. The range on each driver is the low and high you would still defend,
and it is what the sensitivity and scenario work later consumes.

**Under `no-standard-growth-model`,** which is the negative-earnings and trough case, the
sequence is fixed. Build the revenue path from the market and the share. Anchor a target
margin on mature firms with the same business model, state the percentile, and glide to it
with `operating_margin.converge_by`; never anchor on a loss-making current margin. Convert
growth into investment with `sales_to_capital`, which already bundles capital spending,
acquisitions, capitalized research and working capital, so nothing subtracts working capital
again. Pass the loss carryforward and let the engine shelter income until it burns. Deeply
negative early cash flow is the correct output here, not an error.

**8. Set the terminal block deliberately.** The defaults are: growth at or below the riskfree
rate, cost of capital at a mature company's level, and return on capital equal to the terminal
cost of capital, meaning no excess returns. The reinvestment rate is computed by the engine as
growth divided by return on capital, and is never set by hand. Terminal return on capital is
the override most often left on by accident. Raising it above the terminal cost of capital
claims a barrier to entry that survives in perpetuity. Name that barrier in the prose, or set
the two equal.

**9. Value the counter-narrative.** Build at least two alternative stories: one under which
this company is worth far less, and one under which it is worth far more. Express each as the
drivers it changes and the values it changes them to, changing nothing else. Say what evidence
would move you to it. Knowing what someone else's story is worth beats knowing that you
disagree with it, and it usually shows the whole disagreement living in two or three drivers.

**10. Probe the payload, then hand it on.** Run the engine once against a scratch path and
confirm it accepts the payload. Read three diagnostics off the run: the share of value sitting
in the terminal value, the marginal return on invested capital implied over the forecast, and
the terminal excess return.

```bash
python3 <skills>/dcf-valuation-engine/scripts/dcf.py value --in <drivers.json> > <scratch>/probe.json
```

This is a probe, not an artifact. Never write into the intrinsic-valuation directory. If the
engine refuses to run, the refusal is a real error in your drivers — fix the offending input
rather than the output, and probe again. If a marginal return far above what the best firms in
the business earn survives, either the prose carries an argument for it or the sales-to-capital
ratio comes down.

**11. Count twice before writing.** Model inputs with no story sentence: zero. Story claims
with no driver: zero. A claim on two drivers is double counted. A claim on none is decoration.

## Outputs

You write these two files with `write_file` and nothing else. You never edit another stage's
artifact; a disagreement travels as a finding in your return.

**`narrative.md`** — readable on its own, by someone who will not open the JSON. Its sections,
in order:

1. The title that states the bet, then the bias audit.
2. The business map, and the benchmark table with its vintage.
3. The narrative in prose.
4. The claim ledger as a table, with grade, driver and promotion trigger.
5. The screen results, including the aggregation test and the runaway score.
6. The driver table, with a story link on every row.
7. The counter-narratives, and what would move you to each.
8. The open questions.

**`drivers.json`** — the `dcf-valuation-engine` payload plus the blocks that keep the bridge
auditable. `currency` must equal the mandate currency; a mismatch is the most common silent
error in this work.

```json
{
  "base_revenue": 0, "base_ebit": 0, "base_invested_capital": 0,
  "base_year_basis": "cleaned|raw-provisional",
  "forecast_years": 10,
  "revenue_growth": {"start": 0.0, "end": 0.0, "converge_by": 10},
  "operating_margin": {"start": 0.0, "end": 0.0, "converge_by": 5},
  "sales_to_capital": 0.0,
  "tax_rate": {"start": 0.0, "end": 0.0, "converge_by": 10},
  "net_operating_loss_carryforward": 0,
  "cost_of_capital": {"start": 0.0, "end": 0.0, "converge_by": 10, "provisional": true},
  "terminal": {"growth_rate": 0.0, "cost_of_capital": 0.0, "return_on_capital": 0.0},
  "failure": {"probability": 0.0, "proceeds_basis": "book_value|going_concern",
              "book_value_of_capital": 0, "proceeds_percent": 0.0},
  "bridge": {"debt": 0, "cash": 0, "minority_interests": 0, "non_operating_assets": 0,
             "employee_options_value": 0, "shares_outstanding": 0, "current_price": 0},
  "currency": "USD",

  "payload_applies": true,
  "revenue_build": {"total_market": 0, "market_growth": 0.0, "market_source": "",
                    "attained_share": 0.0, "revenue_slice": 0.0,
                    "target_revenue_year_n": 0, "n": 10,
                    "expression": "(target/base)**(1/10)-1", "result": 0.0},
  "driver_ranges": {"operating_margin.end": {"low": 0.0, "high": 0.0,
                                             "reference_class": "", "chosen": 0.0}},
  "story_links": {"revenue_growth": "one sentence", "operating_margin": "one sentence"},
  "claim_ledger": [
    {"id": "C1", "claim": "", "grade": "probable|plausible|possible",
     "evidence": "", "driver": "operating_margin.end", "value": 0.0,
     "range": [0.0, 0.0], "reference_class": "", "reasoning": "",
     "promotion_trigger": "", "source": ""}
  ],
  "screens": {"impossible": [], "implausible": [], "improbable": [],
              "aggregation_test": {"run": false, "implied_sector_revenue": 0,
                                   "market_forecast": 0, "source": ""},
              "runaway_score": {"charismatic_narrator": false,
                                "disliked_status_quo": false,
                                "societal_benefit": false}},
  "option_layer": [{"claim": "", "why_unassessable": "", "owner": "real-options-analyst"}],
  "counter_narratives": [
    {"name": "", "direction": "lower|higher", "drivers_changed": {}, "what_would_move_me": ""}
  ],
  "data_vintage": {"industry_tables_as_of": "", "market_size_retrieved": "",
                   "riskfree_source": "", "sources": []}
}
```

## Constraints

- Paths arrive as inputs. You never hardcode a workspace path or invent a directory.
- No arithmetic in prose. The end-state conversion, the aggregation test and the diagnostics
  run through Python, and the expression is recorded so anyone can re-run it. If a calculation
  you need has no script, say so in the return rather than doing it by hand.
- One claim, one driver, once. A market in the revenue path is not also option value.
- A possible claim never enters the cash flows. It goes to `option_layer` and the
  real-options stage values it, added once on top of the model.
- `terminal.growth_rate` never exceeds the riskfree rate in the mandate currency. Setting it
  lower needs no defence; setting it higher is not available.
- `terminal.return_on_capital` above `terminal.cost_of_capital` requires a named barrier to
  entry in the prose. Otherwise the two are equal.
- Under `require-failure-probability`, the failure block is populated and its proceeds basis
  declared. Never pair a failure probability with a distress-adjusted discount rate; that
  double counts the same risk.
- Under `no-standard-growth-model`, you refuse to state an earnings growth rate at all, and
  build the path from revenue and a target margin instead. Say why in the return.
- Under `no-fcff-valuation`, you refuse to build this payload and name the special-situations
  route. Refusing a forbidden method is correct behaviour, not a failure.
- You do not read the market price before writing the narrative, and if you already have, you
  record that in the bias audit.
- You revise a driver on evidence about the business, never because the resulting value sits
  far from the price.
- Every external number carries its source and its vintage.
- You cannot ask the user anything. When a choice genuinely needs a person, return
  `needs_input` with the question and the options, and the orchestrator asks.

## Return

A structured summary, then one status line as the last line of the answer. Status is
`complete`, `blocked` or `needs_input`. The summary carries:

- **Artifacts written**, as absolute paths.
- **The story in two sentences**, and the title you gave it.
- **The three load-bearing drivers**, each with its value, its range and its reference class.
  These are the ones the answer turns on.
- **Claim ledger counts**: probable, plausible, possible; inputs with no story sentence;
  claims with no driver. The last two are zero or the stage is not finished.
- **Screen results**: anything that failed, and the input you changed in response.
- **Probe diagnostics**: terminal value share, implied marginal return on capital, terminal
  excess return.
- **Provisional fields** the next stage must overwrite: the base-year figures when
  `base_year_basis` is `raw-provisional`, and the cost-of-capital levels when they are marked
  provisional.
- **Constraints honored**, by ID, and what each one changed.
- **Counter-narratives**, each with its direction and the drivers it moves.
- **Option layer**, if any, handed to the real-options stage.
- **Gaps and vintages**: what was missing, what fallback you used, and the date of every
  external table.

Close with the status line:

```
business-narrative-analyst: complete — <company>, <n> claims, <k> drivers set, <currency>
```

When blocked, name the single artifact or field that unblocks you. When input is needed, give
the question and two or three concrete options, so the orchestrator can put it to the user
without a round trip.

