How do I set typical firing counts on a rail?¶
Customization walkthrough — Integrator / Trainer. Shaping the demo.
The story¶
You set amount_typical_range last week and the per-swipe numbers look right — $5 to $500, clustered low, exactly the shape of a coffee-shop card book. Then the demo went sideways again: the L1 Daily Statement top-line shows $2.5M in card sales per day. The auditor — who knows this is a 200-customer community bank — raises an eyebrow. "You process two and a half million dollars a day in card volume?"
You don't. The per-firing amount is realistic, but the generator fired 50,000 card sales a day — the per-kind firing-count heuristic scaled UP on customer count and produced an institution-wide volume an order of magnitude too large. Per-firing realism alone never fixes the top-line: count × amount = aggregate, and the count half was UNBOUNDED.
That's what firings_typical_per_period
bounds — the complement to amount_typical_range. You declare how
many times the rail typically fires per period, and the generator
samples within that band instead of falling back to the heuristic.
The question¶
"How do I make MerchantCardSale fire ~50-500 times per business day (realistic for a small bank) instead of the heuristic's 50,000?"
Where to look¶
Three reference points:
- Rail (concept) → Optional: typical firing-count range
— the field semantics: the two YAML shapes, how the per-period
sampler scales to the window, what the W1a-c validator rules enforce
and how it composes with
amount_typical_range. tests/l2/spec_example.yaml— the minimal fixture carries the compact form (ExternalRailInbound: [20, 50]), the mapping form (SubledgerCharge: {period: month, range: [60, 90]}) and the field on a TransferTemplate (SettlementTimingCycle: {period: week, range: [3, 8]}). Search forfirings_typical_per_period.run/sasquatch_pr.yaml(or your own L2 yaml underrun/) — the bundled real-world example carries NO firing-count bands — every rail falls back to the count heuristic.MerchantCardSale,CustomerInboundACHandInternalTransferDebitare the rails you'd addfirings_typical_per_periodto; worked forms live in thetests/l2/spec_example.yamlbullet above.
The change¶
In your run/<institution>.yaml, find the rail and add
firings_typical_per_period. Compact form (defaults to per business
day):
rails:
- name: MerchantCardSale
source_role: ExternalCardNetwork
destination_role: MerchantPayableClearing
metadata_keys: [merchant_id, settlement_period, card_network_ref]
amount_typical_range: ["5.00", "500.00"]
firings_typical_per_period: [50, 500] # 50-500 swipes / business day
For a non-daily cadence, use the full form with an explicit period
(business_day | pay_period | week | month):
- name: SomeMonthlyRail
leg_role: CustomerSubledger
leg_direction: Debit
firings_typical_per_period:
period: month
range: [80, 120]
Two notes on shape:
- Counts are integers.
minMUST be<= max(W1a) — equal endpoints are fine ([1, 1]= "exactly one per period"). Both MUST be>= 0(W1b) — zero is allowed (a rail that some periods doesn't fire at all). - The field is FORBIDDEN on rails with
aggregating: true(W1c). An aggregating rail'scadencealready governs how often it fires; set the count band on the child rails it bundles instead.
How to verify¶
Re-seed the demo, then refresh the matviews so the dashboards see the new rows:
recon-gen data apply -c run/config.yaml --execute
recon-gen data refresh -c run/config.yaml --execute
The seed regenerates demo Transactions with the per-period sampler honoring your band. Open the L1 dashboard and read the Daily Statement top-line — the card-sales aggregate should now be (50-500 swipes/day) × (\$5-\$500/swipe) ≈ a few thousand to a couple hundred thousand dollars a day, the realistic range for a small bank, instead of the $2.5M the heuristic produced.
To confirm the count specifically, filter the L1 transactions view to
rail_name = MerchantCardSale and group by the posting day — the
per-day row count should land inside your declared band.
What you should NOT do¶
- Don't set
firings_typical_per_periodon an aggregating rail. Validator W1c rejects this at load time. Thecadencefield already encodes the aggregator's firing frequency (one firing per cadence-period). Set the band on the child rails instead. - Don't set
min > max(W1a rejects). Equal is fine; descending is operator confusion. - Don't set negative counts (W1b rejects). Zero is allowed.
- Don't expect the count to be exact per day. The band targets the PERIOD, not the day; the generator spreads it across the period's business days with its existing Poisson distribution, so individual days vary around the average. What lands inside the band is the aggregate-per-period.
- Don't expect runtime enforcement yet. Like
amount_typical_range, this is a generator-shaping hint (and a future_volume_anomalymatview hook) — not a hard constraint on real data. Real-world periods whose count falls outside the band will surface in the follow-on matview when that lands.
Related¶
- How do I set typical amount ranges? — the per-firing magnitude bound. Compose the two: realistic amounts × realistic counts = a realistic per-period aggregate.
- Rail (concept) — field-by-field semantics, the W1a-c validator rules and the period-to-window conversion ratios.
- Schema_v6 → Volume as data — the data contract for the firing-count band.