ActuarialOS

ActuarialOS for Excel

Function Reference

Signature, plain-English use, and the method, convention and engine anchor behind every function. The same guide the ribbon's Help dialog opens in Excel.

Source
Published verbatim from the add-in’s own FUNCTIONS.md

The complete guide to the aos_* worksheet functions. Two audiences:

Verify before signing. Functions reproduce the engine exactly, but some embedded DATA is an attested input — see § Verify before signing. The cross-check's current case count and max relative error (the verifier of record) are stated in the add-in's governing doc, Addin/PROJECT.md, and printed by every verifier run.

Conventions used throughout


Commutation

FunctionUse it forMethod / anchor
aos_Dx(age, rate, mort, [blend], [method])Discounted survivors Dₓlₓ built from a radix at the table's youngest defined age (ratio = ActOS survival annuities); blend = an optional second range averaged 50/50. method is aos_Nx12's fractional-age basis (2026-09-25, AP8 / Q7 — it was accepted and not read, and aos_Nx12 always computed Nx − 11/24·Dx while the Commutation dialog defaulted to and emitted "UDD"): `"1124"` (the default when omitted) = Nx − 11/24·Dx, the traditional approximation; `"UDD"` = α(12)·Nx − β(12)·Dx, exact under a uniform distribution of deaths at the column's rate, α(12) = i·d / (i⁽¹²⁾·d⁽¹²⁾), β(12) = (i − i⁽¹²⁾) / (i⁽¹²⁾·d⁽¹²⁾) (β = 0.46651 at 5%, against 11/24 = 0.45833); any other text REFUSES (#AOS: unknown fractional-age method …). A saved `"UDD"` formula moves (it computed 11/24). The dialog now defaults to "1124". aos_Dx/Nx/Cx/Mx do not read it; aos_CommTable's Nx12 column is the "1124" default. Same gap refusal as the columns below.
aos_Nx(age, rate, mort, …)Σ Dₓ (annuity-due numerator)aos_Nx/aos_Dx reproduces the engine annuity-due to machine epsilon.
aos_Nx12, aos_Cx, aos_MxMonthly Nx; Cx / Mx commutationStandard commutation columns on the same lₓ basis. A mortality range with a GAP inside its span (a blank, text, or a qx outside 0–1 at some age between its first and last) REFUSES for every column — #AOS: the mortality range has no valid qx at age N …, naming the first gapped age (2026-09-24, AI2; it used to return #NUM! from the gap down, and the columns below it were built from a broken lₓ chain). A non-finite value from an extreme rate (near −1) returns #NUM!.
aos_Mort(table, [ageAdj], [scale], [calYear])Build the mortality argumentStatic projection to calYear by a band or 2-D generational scale (== genImprovementFactor).
aos_CommTable(minAge, maxAge, rate, mortality, [blend], [header])Spilling commutation tableSpills Age, Dx, Nx, Cx, Mx, Nx12 — one row per integer age over [minAge, maxAge], with an optional header row. Same kernel as aos_Dx/aos_Nx/… (so Nx/Dx reproduces the engine annuity-due); ages outside the table's defined range return #NUM. A range with a gap inside its span REFUSES the whole spill (#AOS: …, naming the first gapped age — 2026-09-24, AI2; it used to spill NaN through every Nx and Mx cell), and no cell ever carries a non-finite number (an extreme rate's overflow shows #NUM! in that cell). A dynamic-array formula — enter once and it spills.

Mortality & scale

FunctionUse it forMethod / anchor
aos_qx(table, x, [y], [ageAdj], [scale], [year], [gen])Mortality rate qOne-year q(x), or (y-x)q(x) = 1 − l(y)/l(x) when y is given. q == genQx. Static or generational (gen). Under a named scale, a calendar year BEFORE the improvement start (the later of the table's published base year and the scale's own — Pri-2012 under MP-2021: 2012; RP-2014: 2014; GAM-83 under Scale AA: 2000) REFUSES (#AOS: the "…" improvement scale was asked for calendar year … improvement on that table starts from …, ActuarialOS 2026.06.194) — it used to price the unimproved base rates; the start year itself is the base rates; scale "None" is inert at any year.
aos_px(table, x, [y], [ageAdj], [scale], [year], [gen])Survival probability pOne-year p(x), or (y-x)p(x) = l(y)/l(x) when y is given.
aos_lx(table, x, [ageAdj], [scale], [year], [gen])Life-table survivors l(x)Radix-100000 life table from the table's min age, built from genQx. Generational (gen=TRUE) under a named scale: the radix sits at the min age in calendar year year − (x − min age), so a radix year BEFORE the improvement start REFUSES, as aos_qx does.
aos_Deaths(table, x, [y], [ageAdj], [scale], [year], [gen])Life-table deaths d(x)l(x)·q(x) one-year, or l(x) − l(y) when y is given. (Named aos_Deaths, not aos_dx: Excel is case-insensitive and aos_Dx is the commutation column.) Built on aos_lx, so a generational radix year BEFORE the improvement start REFUSES as it does.
aos_Scale(scale, age)Improvement rate at an age== getImprovementRate (the band scales: MP-2021, MP-2020, None — Scale AA is the published 2-D scale since ActOS wave 2c E5, and refuses here as one). An unknown scale name REFUSES (#AOS: unknown mortality improvement scale …, listing the scales carried — 2026-09-25, AL4; it used to read MP-2021's rates, here and in every band projection a misspelled scale reached). A 2-D generational scale (e.g. MP-2019) also refuses here — its rate varies by year; project on it through aos_qx / aos_ax.
aos_Blend(A, B, w, [wB])A weighted two-table blend id "A|B|w" (or "A|B|w|wB")Understood by GenQx, so ANY function can use w·A + (1−w)·B. Give wB explicitly for a weighted SUM whose weights need not total 1 (e.g. 0.6045·M + 0.5525·F adjusted-table blends); each component projects on its own sex's scale. The composition is bounded (2026-09-25, AU3 — ruling AR3 (a)): where BOTH components sit at their terminal closure (each reads 1.0 at or after its own last age) the blend reads 1.0, whatever the weights sum to — the 0.6045 + 0.5525 idiom used to compose 1.157 there, so aos_qx printed 1.157, aos_Deaths exceeded l(x) and a GAM-83 aos_lx went negative past 110; any other composed qx above 1 REFUSES (#AOS: mortality id … composes qx = …); a negative weight (or w above 1 in the convex form) REFUSES. Annuity factors move only in the last year of the table, and only where survival to it is not already negligible (the golden AE beneficiary blend, weights summing 0.623, moved −5.7e-7 relative: its closure used to read 0.623, a survivor past the end of both tables).
aos_Splice(preTable, postTable)An active→in-pay spliced table id ("pre>post")The PRE (active / pre-commencement) table up to its last published age, the POST (in-pay) table above it — usable as any function's table argument (both annuity builders emit it; e.g. aos_ax(aos_Splice("Pri-2012 Employee Male", "Pri-2012 WC Retiree Male"), 45, 0.05)). A bare Employee table already auto-extends with its default in-pay partner; aos_Splice picks a DIFFERENT one. Either id unknown → #NAME?. A read below the PRE table's first age still REFUSES (#AOS: … carries no rate at age N — the span refusal; wrap with aos_Floor for survivable pre-eligibility ages). A wrapped PRE (aos_Project / aos_ProjectGen / aos_Floor of a table) cuts at the INNER table's published last age, and a registered custom PRE (aos_RegisterTable) at its last defined age (2026-09-25, AU2 — both used to cut at 120, so a projected Employee PRE read its baked default partner above 80 instead of the chosen POST: a saved splice over a wrapped or custom PRE moves). A blend PRE has no published last age and REFUSES (#AOS: the splice's PRE table … is a blend …) — name a single table as the PRE.
aos_Floor(table)Survivable pre-eligibility ages ("FLOOR~…" id)Wraps any table/blend/splice id so ages BELOW its first defined age get qx = 0 instead of the span refusal (without it, a read below the first age REFUSES — #AOS: … carries no rate at age N, since 2026-09-22) — e.g. a Retiree (50+) table in a conversion-factor grid spanning ages 20+. At/above the floor, identical to the inner id.
aos_Table(ages, rates, [gender], [baseYear])A custom table from cell ranges (inline)Builds a CUSTOM|… id; reproduces a named table's annuity exactly (incl. scale projection). gender is "M" (default), "F", or `"U"` for a UNISEX table — under a 2-D generational scale a unisex table is improved on the 50/50 blend of the male and female cumulative improvement factors (the IRS unisex construction; "U:0.6" states another male weight). baseYear states the year the supplied qx apply — where improvement is projected FROM (e.g. 2014 for RP-2014 rates), appended to the id as |by=<year>. It is REQUIRED to use the table with an improvement scale: a scale applied to a table that declares no base year is REFUSED, never assumed — omit it only when no scale will be applied (then nothing reads it and the answer is unchanged).
aos_RegisterTable(name, ages, rates, [gender], [baseYear])Name a custom table → reference it by namePut in a cell; then any function resolves the name, e.g. =aos_qx("MyPlan", 65), and it appears in aos_Tables(). The rate DATA stays in the ranges (reproducible — no hidden machine state). Session-global names; reference the register cell (or recalc) so dependents refresh. baseYear has the same meaning — and the same requirement under an improvement scale — as aos_Table's.
aos_QxTable(table, [minAge], [maxAge], [scale], [calYear], [header])Spilling raw mortality table (verify / compare / view)Spills Age, qx, lx, tPx — the EXACT qx the engine uses (optionally projected by scale to calYear), for verifying a table against the SOA publication or comparing against another tool. Omit `minAge`/`maxAge` to view the whole table (defaults to the table's full defined age range), e.g. =aos_QxTable("Pri-2012 Male"). qx == genQx; lx is a radix-100000 life table from minAge; tPx is survival from minAge. Dynamic-array spill. The Mortality dialog's "full table" function builds this.
aos_Project(table, [scale], [baseYear], [targetYear])Static-projection table idBuilds a "PROJ~…" id: the table improved by scale from its own published base year to targetYear and FROZEN at that level (the same rate every payment year) — the id reads exactly the targetYear rate. baseYear is optional and, if stated, must BE the table's published base year: any other year REFUSES (#AOS: …, naming both; ADDIN-C, Scott's ruling 2026-09-29 — it used to count from the stated year and replay from the table's, freezing at a year nobody typed, e.g. GAM-83 stated 2012 → 2020 froze at 1991). To measure improvement from a different base year use aos_ProjectGen. A targetYear before the table's base year under a scale REFUSES (it used to freeze at the base year). Use as any annuity/commutation table arg (pass scale="None" there — the projection is baked in). A table that declares no base year (a CUSTOM|… spec with no by=) is refused under any scale — state it on aos_Table / aos_RegisterTable, or use a published table. The id replays at the table's own base year + (targetYear − that base year) = targetYear; where targetYear falls BEFORE the improvement start (the later of the table's base year and the scale's — e.g. GAM-83 under MP-2021 with a targetYear before 2012) every read REFUSES (ActuarialOS 2026.06.194; it used to freeze the unimproved base rates).
aos_ProjectGen(table, [scale], [baseYear])Generational projection re-based to a base yearBuilds a "GENBASE~…" id: generational improvement (keeps advancing each future payment year, unlike aos_Project) measured from baseYear instead of the scale's intrinsic 2012 anchor. baseYear = the improvement start (the later of the table's published base year and the scale's own — the default) reproduces the plain generational result (so a bare scale arg); a baseYear BEFORE the improvement start REFUSES at the door (#AOS: …, ActuarialOS 2026.06.194 — the default used to be the table's own base, which for GAM-83 / UP-94 / GATT-83 under an MP scale now precedes the start). Use it to value a table whose rates are as-of a year other than 2012. Pass scale="None" on the consuming function (the scale is baked in). A table that declares no base year is refused under any scale, exactly as in aos_Project. A re-based rate is the plain one divided by the scale's improvement factor from the table's base to baseYear; where that lifts it above 1 it REFUSES (#AOS: mortality id … composes qx = …, 2026-09-25, AU3 — measured reachable only with a far-future baseYear near a table's end — at calendar 2025, GAM-83 / GATT-83 at ages 107–109 with baseYear 2073 or later).

Annuities — core

FunctionUse it forMethod / anchor
aos_ax(table, age, rate, [valYear], [scale], [cola], [commence], [freq], [timing], [beforeTable])PV of a $1/yr single-life annuity== pvInPayLife. Deferral is engine-anchored (deferral survival × vⁿ × annuity-at-commencement); exact m-thly (1/12) with UDD; beforeTable = before/after-commencement mortality.
aos_axy(tableA, ageA, tableB, ageB, rate, …)PV of a joint-life annuity (pays while BOTH alive)== pvInPayJoint.
aos_axVector(table, ages, rate, [valYear], [scale], [cola], [commence], [freq], [timing], [beforeTable])Vectorized `aos_ax` (spills)One aos_ax value per age in the ages range, spilling in the range's row/column orientation. Same kernel as aos_ax. Omit commence for an immediate annuity at each age; pass a scalar commence to defer every age to that fixed commencement.
aos_axTrace(table, age, rate, [valYear], [scale], [cola], [freq], [timing], [header])Explain this number (spills)The term-by-term decomposition of a $1/yr immediate single-life annuity: Age, PayTime, tPx, Discount, COLA, Payment, Term, Cumulative — one row per payment, then a TOTAL row. The Term column sums to aos_ax (mirrors that kernel exactly), so a reviewer can see survival × discount × COLA build the number. For peer review and teaching.

Annuities — optional forms

FunctionUse it forMethod / anchor
aos_axc(table, age, certainYears, rate, …)n-year certain & lifePayments in [0,n) guaranteed (weight 1), then life-contingent — via the generalized AxForm kernel.
aos_axt(table, age, tempYears, rate, …)n-year temporary life annuityNo payments at/after time n.
aos_axJS(memberTable, memberAge, survivorTable, survivorAge, jsPct, rate, …)Contingent J&S ($1 to member, jsPct to survivor)== ActOS ä_member + jsPct·(ä_survivor − ä_joint).
aos_axRefund(table, age, refundBase, rate, [valYear], [scale], [cola])Modified cash refund life annuityLife annuity + PV of the at-death shortfall: if total payments < refundBase at death, the lump-sum shortfall is refunded at end of the death year (completed payments counted). refundBase=0 == plain life annuity (anchor). Annual. Mechanical convention; installment variant = `aos_axInstallmentRefund`; contribution-base refund is the user's.
aos_axInstallmentRefund(table, age, refundBase, rate, [valYear], [scale], [cola], [freq], [timing])Installment-refund life annuityPayments GUARANTEED (continue to a beneficiary) until cumulative reaches refundBase, then life-contingent — a refund-certain annuity = certain-&-life with the recovery period (n = refundBase level; ln(1+cola·refundBase)/ln(1+cola) with COLA). refundBase=0 == plain life. Cash-refund variant: aos_axRefund.
aos_axSSLevel(table, age, ssAge, rate, …)SS level-income factor LL = (deferred-to-ssAge annuity)/(immediate annuity). Pre-ssAge pension = base + SS·L; post-ssAge = that − SS. ssAge==age → L=1 (anchor).
aos_ConvFactor(memberTable, memberAge, survivorTable, survivorAge, jsPct, rate, …, [popup])Single-life → J&S conversion factor= ä(single life)/ä(J&S). Multiply a single-life benefit by this for the actuarially-equivalent J&S benefit. jsPct=0 → 1 (anchor). popup=TRUE → pop-up J&S (the member reverts to the full single-life benefit if the survivor dies first): factor = ä_joint/(ä_joint + jsPct·(ä_survivor − ä_joint)). Actuarial-equivalence baseline — a plan's PUBLISHED factor table overrides it and is user-supplied.
aos_ConvFactorForm(table, age, form, param, rate, …)Single-life → optional-form conversion factor= ä(single life)/ä(form). form = "certain & life" / "temporary" / "certain only" / "cash refund" / "installment refund"; param = the guarantee/term years or the refund base. Anchors: "certain & life" with param=0 → 1; "temporary" covering the whole life → 1. Cash refund is annual (matches aos_axRefund). Same equivalence baseline — published factors override.
aos_LumpSum417(commenceAge, commenceYear, rate, [cola], [freq], [maleTable], [femaleTable], [scale], [applicableTable])§417(e) minimum lump-sum factorPer $1/yr of annual life annuity at commencement: an annuity-DUE on the applicable basis (rate = the 3-segment §430 triple for the lookback month, or a curve / flat) with the unisex applicable mortality (50/50 M/F blend projected STATICALLY to commenceYear for every payment — the §417(e)(3) table is prescribed per calendar year, Reg. §1.417(e)-1(d)(2); ActOS 2026.06.204, generational to each payment year through .203 — default Pri-2012, pass the MP scale). Accrued annual benefit × factor = the minimum lump sum. Reproduces the engine's lumpSumLifeFactorAt (engine-anchored). Pick the lookback-month rates via aos_EconMonthlyLookup.
aos_ERF(retireAge, nra, reductionPct, [subsidized], [service], [unreducedAge], [unreducedService], [unreducedPoints])Plan early-retirement reduction factorreductionPct per year early from NRA (or from an unreduced age when the service/points condition is met — Rule-of-X), floored at 0; subsidized=TRUE or retireAge ≥ NRA → 1. reductionPct omitted or 0 = no reduction (factor 1.0). Reproduces the engine's earlyRetirementFactor. This is the PLAN's stated reduction formula; a plan's tabulated ERF table overrides it and is user-supplied.

Annuities — mid-of-year (fractional age)

There are two MOY conventions (they agree only at an integer age, where both reduce to pvInPayLife/pvInPayJoint):

FunctionUse it forMethod / anchor
aos_axMOY(table, age, rate, [valYear], [scale], [cola], [colaMethod])Single life on the member's anniversary scheduleAnnual payments at t = 1−g, 2−g, … from a fractional age a0+g; UDD partial-first-year survival; colaMethod = full or partial-year. Integer age == pvInPayLife.
aos_axMOYm(table, age, rate, …, [freq], [timing], [commence], [beforeTable], [colaPartial], [monthlyBasis])Single life, forward from valuation (monthly / deferred)Payments at regular 1/m intervals forward from the valuation date; UDD survival from the fractional age. Integer age == aos_ax; integer-age/commence deferral == aos_ax deferred. Level/aos_Cola COLA (no partial-year). monthlyBasis = "Exact" (default, the engine anchor) or "Woolhouse" (2-term ä⁽ᵐ⁾ = ä − (m−1)/2m — ties to legacy valuation tools; at freq=1, immediate it gives ä − 1).
aos_axyMOY(tableA, ageA, tableB, ageB, rate, …, [colaPartial], [monthlyBasis])Joint, forward from valuationPays while both survive; one schedule from valuation. Integer ages == aos_axy. monthlyBasis = "Exact" (default) or "Woolhouse" (legacy-tie), as for aos_axMOYm.
aos_axMOYStatus(table, age, status, rate, …, [monthlyBasis], [commence])Valuation 3-status mid-year modelstatus = Active (commence + value at the mid-year point; attained age = age+0.5), Inactive-Deferred (commence at the middle of the commencement year, value at BOY), or Inactive-InPay (commence + value at BOY). Reproduces a legacy valuation tool's mid-year statuses. Integer-age InPay == aos_axMOYm.

Monthly basis (`aos_axMOYStatus`). The sub-annual method is selectable: `Woolhouse` (default) uses the 2-term Woolhouse approximation ä⁽ᵐ⁾ = ä − (m−1)/2m — the industry standard (SOA factor tables, most valuation systems) and what legacy tools emit, so in-pay factors tie out; `Exact` sums the m monthly payments under UDD (the add-in's engine-anchored convention), which sits ~0.0055 lower for monthly — that gap is exactly the third Woolhouse term (the (δ+μ) mortality-curvature correction). Pick the basis your plan/peer-review/audit expects. The mid-year first half-year is always valued exactly (the 6 monthly payments), so Active/Inactive-Deferred may differ by ~1e-4 from a legacy tool that approximates the first half-year with a 7/24 + 5/24 closed form — a deliberate, more-exact choice.


COLA & interest helpers

FunctionUse it forMethod / anchor
aos_Cola(selectRates, ultimate, [max])A select-and-ultimate COLA scheduleThreaded through all annuity kernels; an ultimate-only schedule equals the level path.
aos_SegmentDF(t, seg1, seg2, seg3)§430 three-segment discount factor at t(1 + seg_k)^−t at the ONE rate of the period t falls in — seg1 on (0,5), seg2 on [5,20), seg3 on [20,∞); a SPOT rate, not chained (ENGINE-N) — == segmentDiscountFactor.
aos_Curve(years, rates, [compounding], [valuationOffset])Build a spot-curve interest specFor the rate arg of any annuity; == yieldCurve.js (linear interp, flat-extrapolated). compounding = "annual" (default, (1+s)^-t) or "semiannual" ((1+s/2)^-2t, the PPA "half year rates" convention). valuationOffset = years from the curve's as-of date to the attained-age point (default 0 = value at the curve date); >0 discounts on the curve's FORWARD segment, normalized to the attained age — the "PV Factor at Decrement Age" a funding tool (fxAct/Quantify) reports for a deferred/mid-year-commencing benefit. A DEFERRED annuity on a curve (a commence later than the age, or a deferred status) is discounted from the VALUATION date — a payment t years after a commencement n years out at D(A + n + t) / D(A), A = valuationOffset — exactly as a §430 segment triple is (ADDIN-C, Scott's ruling 2026-09-29; before it the add-in restarted the curve at commencement, D(A + n) · D(A + t) / D(A)²). That is a value AS OF the valuation date, survival and discount to commencement included. A funding tool's factor AS OF commencement (per $1 at the decrement age) is instead: value as Inactive-InPay at the commencement age with valuationOffset = years from the valuation date to commencement — and do NOT combine that offset with a deferral as well (the origin would count the deferral twice).
aos_AdjustCurve(curve, schedMaturities, schedBps)Apply a maturity-dependent bps spread schedule to a curve speccurve = a spec string (aos_Curve / aos_HqmCurve / aos_TreasuryCurve / aos_Pbgc4044Curve) or a 2-column [maturity, rate] range; returns an adjusted spec for any annuity's rate arg. The spread is interpolated between schedule maturities and held flat beyond; negative bps allowed; a single-entry schedule is a parallel shift; the spec's compounding/valuationOffset tail is preserved. == rate-engine adjustCurve (golden cross-checked). This is a user-supplied adjustment — label and justify it; it is never a bond-universe percentile selection.
aos_SpotRate(t, years, rates)Spot rate at maturity t== spotRateForYear.

Economic / statutory / PBGC

FunctionUse it forMethod / anchor
aos_EconAnnualLookup(table, year, [indexRate])IRS qualified-plan limits suitetable = "401(a)(17)"/"comp", "415(b)"/"benefit", "415(c)", "402(g)", "414(v)"/"catch-up", "414(q)"/"HCE", "416(i)"/"key", or "wage base". == statutoryLimits.js (2020–2026, IRS cola-table + Notice 2025-67); a year after the table is a projection made only at a STATED `indexRate` — compounded, then floored to the limit's statutory step (engine 2026.06.79 — $5,000 comp/415(b)/HCE/key, $1,000 415(c), $500 402(g)/414(v)); with `indexRate` omitted, an unpublished year REFUSES (#AOS: no IRS … limit is published for YYYY …, naming the years on file — 2026-09-24, N8; it used to default the index to 0 and hold the last limit flat). A stated 0 is a stated flat projection and values, and a negative index projects flat — limits never decrease (Notice 2009-94: a decline in the applicable index cannot reduce the limit; ruled 2026-09-25, P8). Years before the table read its first year. Wage base single-sourced from covered comp; the §230 taxable wage base NEVER projects, whatever index is given — a year after the last published base REFUSES with or without indexRate (42 USC 430(b) indexes the base on the national-average-wage ratio, not a CPI-type rate; ruled 2026-09-25, P2).
aos_Limit415Adjusted(commenceAge, year, [rate], [maleTable], [femaleTable], [scale])§415(b) dollar limit, age-adjustedThe §415(b) DB dollar limit adjusted to actuarial equivalence outside the 62–65 plateau, WITH the deferral (ActOS 2026.06.195, OR2-R1 — Reg. §1.415(b)-1(d)(1), (e)): before 62 L · ä(62) · v^n · ₙp_x / ä(x) (n = 62 − x, reduced); after 65 L · ä(65) / (v^m · ₘp₆₅ · ä(x)) (m = x − 65, increased); L = benefitLimit(year), x = the commencement age rounded half-up, and ä is an annual life annuity-DUE on the §415 basis (default 5% + unisex 50/50 applicable mortality, generational via scale, the survival decrement applied). Through .197 this was L · ä(ref)/ä(commence) — two immediate annuities, no deferral; saved cells outside 62–65 MOVE at the rebuild. The §415(b)(1)(B) 100%-of-compensation prong is its own function, aos_Limit415Comp; the §415(b) limit is the lesser of the two. Ages 62–65 return L unchanged. year must have a published §415(b) limit — this function takes no index, so an unpublished year REFUSES (#AOS: …; 2026-09-24, N8 — it held the last limit flat). The §415(b)(2)(E) "lesser/greater of plan basis vs 5%+applicable" comparison is the user's. An unknown maleTable / femaleTable id returns #NAME? at every age (2026-09-24), like every table-taking function — never a limit priced on a half-missing blend.
aos_Limit415Comp(high3, [serviceYears])§415(b)(1)(B) compensation limit100% of the participant's average compensation for the high-3 years, prorated for years of service under 10 (§415(b)(5)(B)) and never below 1/10 (§415(b)(5)(C)): high3 × max(min(serviceYears / 10, 1), 0.1) — the engine's own expression (capBenefit; ADDIN-C, Scott's ruling 2026-09-29). serviceYears omitted = no proration (factor 1), as the engine reads an unstated service. Not age-adjusted. high3 is an input: the average for the high-3 years with each year's pay already capped at §401(a)(17) — the engine caps and averages the pay history itself (high3Comp415); a worksheet function cannot see it. The §415(b) limit is the lesser of this and the dollar limit (aos_Limit415Adjusted). A high3 that is not a positive amount REFUSES (#AOS: …) — the engine reads it as no pay stated and applies no compensation prong, and a $0 limit would be a confident wrong answer; a non-numeric serviceYears is #VALUE!.
aos_CoveredComp(yearOfBirth, [determinationYear], [rounded])IRC §401(l)(5)(E) covered compensationThe 35-year average of the SSA taxable wage bases ending in the year the employee attains social security retirement age (the §415(b)(8) whole-year SSRA: 65 for birth < 1938, 66 for 1938–1954, 67 for 1955+). Wage bases for years after determinationYear are held at the determination-year base; unrounded = floor(35-yr average / 12) × 12. rounded=TRUE → the IRS rounded table (nearest $3,000, capped at the wage base). Cross-checked to the dollar against IRS Rev. Rul. 2026-1 (the SSA wage-base series is validated by the exact match over all 87 birth years — see coveredComp.test.js). determinationYear defaults to the latest published wage base (2026). What the regulation holds, and what it does not (Q9, ruled 2026-09-25): Treas. Reg. §1.401(l)-1(c)(7)(i) assumes the wage base for every year AFTER the determination year, inside the 35-year average, equals the year-D base — it does not say which table an unpublished determination year uses; that is a plan election. So a determination year after the last published wage base REFUSES (it used to be held at the latest table, on that citation — N8's "ruled hold", reversed), like the limits and the PBGC values. A determination year before the first wage base (1937) has no base either and REFUSES (#AOS: no §230 taxable wage base is published for …; 2026-09-25, AL3 — it used to print the text #ERR The given key was not present in the dictionary.). The span: birth years whose 35-year window starts in 1937 or later — a birth year whose 35-year window starts before 1937, the first wage base (births 1905 and earlier under SSRA 65), REFUSES (#AOS: no §230 taxable wage base is published for 1936 …; 2026-09-25, AU4 — ruling AR4 (a): it borrowed the 1937 base for the missing years, a value §1.401(l)-1(c)(7)(i) does not state; the IRS tables start at birth year 1907). 1906 answers 4,320 and 1907 4,488 (2026 determination year).
aos_EconMonthlyLookup("Segment 1|2|3"|"24-mo Segment …", year, month)§430 segment rates (spot or 24-mo avg)The 24-mo average FOR month M is the mean of the 24 months M−24 … M−1 (§430(h)(2)(D)(i), "ending with the month preceding"; through ActOS .211 it read M−23 … M — matches the IRS published table since .212). The average is taken at two decimals of a percent, half-up (the rate of record since ActOS .231; the add-in since AN1, 2026-10-18 — it returned the unrounded mean). Reproduces getSegmentRates/get24MonthAverageRates on every published month — not where a month is missing: ActuarialOS substitutes and flags that case (substituted / monthsMissing), the add-in has no flag channel and refuses. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; AI1, 2026-09-24, R7's rule carried to this family). A 24-month average needs all 24 months published — a window reaching a month not on file REFUSES, naming the first missing month (it used to average only the months it found; the ActOS side flags the same case monthsMissing).
aos_SegRate(years, year, month, [basis], [planYear])§430 segment rate by durationThe segment is chosen by the time to payment — under 5 yrs = seg 1, 5 to under 20 = seg 2, 20 and over = seg 3 (the boundary aos_SegmentDF applies; through ActOS .205 exactly 5 and 20 read the EARLIER segment) — and returns that segment's rate. basis = spot (default) / 24mo / stabilized / 25yr. Each basis REFUSES what it cannot state (2026-09-24, AI1): an unpublished month (spot), a 24-month window not wholly published (24mo, stabilized), and a plan year with no published 25-year averages (25yr — it used to show an untyped #VALUE! — and stabilized, which used to return the unclamped 24-month average).
aos_Seg25Avg(planYear, seg)§430 25-year average segment rateIRS Table 2A (incl. ARPA 5% floor), decimal; seg 1/2/3. Returns `#N/A` for a plan year the IRS has not published — it no longer holds the nearest year's rate forward. Attested input — verify.
aos_SegCorridor(planYear, "min"|"max")MAP-21/ARPA/IIJA stabilization corridorApplicable min/max fraction: 95%/105% for 2020–2030, then widening 5%/yr to 70%/130% after 2034; 90%/110% pre-2020. (Corrected 2026-08-06 — this previously widened from 2026, i.e. ARPA §9706(a)(1) as enacted, missing IIJA §80602, which extended 95%/105% through 2030. Verified vs IRS Funding Table 1A and Notice 2026-19.)
aos_SegStabilized(planYear, rateYear, rateMonth, seg)Stabilized §430 segment rateThe 24-mo average for rateYear/rateMonth clamped to the corridor around the 25-yr average; == stabilizeSegmentRates. The rounded average is compared with the floor / ceiling and the figure taken is at two decimals of a percent, half-up (ActOS .231; the add-in since AN1, 2026-10-18). REFUSES a lookback window not wholly published, and a plan year with no published 25-year averages (2026-09-24, AI1 — it used to return the unclamped 24-month average as though stabilized; the ActOS run path refuses the same case).
aos_Pbgc4044(year, month, maturity)PBGC §4044 monthly spot ratePublished month-end curve (series begins 2024-07); reproduces spotRateForYear. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24). Attested input — verify.
aos_Pbgc4044Curve(year, month)PBGC §4044 curve spec (interest arg)A curve usable as any annuity's rate arg. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24).
aos_Hqm(year, month, maturity)Treasury HQM monthly spot curve — spot rateThe Treasury HQM corporate-bond monthly spot curve, the source of the §430 segment rates; published monthly spot (0.5–30, flat beyond), reproduces spotRateForYear. Not the full §430 curve: Reg. §1.430(h)(2)-1(d)(1)(i) runs the §430(h)(2)(D)(ii) full yield curve from 6 months to 100 years; the add-in carries the HQM curve as published, 0.5–30 years (flat beyond 30), and ActuarialOS builds no full-yield-curve election yet. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24). Attested input — verify.
aos_HqmCurve(year, month)Treasury HQM curve spec (interest arg)Value an annuity directly on the Treasury HQM monthly spot curve (0.5–30, flat beyond) — usable as any annuity's rate arg. Same curve as aos_FullYieldCurve. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24).
aos_FullYieldCurve(year, month)Treasury HQM curve spec under its funding name (interest arg)The HQM curve as a curve spec (== aos_HqmCurve). It is not the §430(h)(2)(D)(ii) full yield curve a plan may elect: Reg. §1.430(h)(2)-1(d)(1)(i) runs the §430(h)(2)(D)(ii) full yield curve from 6 months to 100 years; the add-in carries the HQM curve as published, 0.5–30 years (flat beyond 30), and ActuarialOS builds no full-yield-curve election yet. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24).
aos_HqmTable(year, month, [header])HQM spot curve, spillingSpills Maturity, Spot for every published half-year tenor (0.5–30) — see/plot what the add-in carries (the full §430 curve runs to 100 years). Each Spot == aos_Hqm at that maturity. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; R7, 2026-09-24).
aos_Treasury(year, month, maturity, [spreadBps])Treasury PAR yield (month-end CMT)Published Daily Treasury Par Yield Curve Rates, month-end; interpolated, flat beyond 30. PAR basis (semiannual coupon-equivalent), NOT a spot rate — for a corporate spot curve use aos_Hqm. Optional parallel spread in bps ("Treasury + spread"); numeric text such as "25" reads as 25, and non-numeric text, TRUE/FALSE or an error value is #VALUE! (AP2, 2026-09-25 — it read as a 0 spread). An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; AI1, 2026-09-24, R7's rule carried to this family). Attested input — verify vs home.treasury.gov.
aos_TreasuryCurve(year, month, [spreadBps])Treasury par curve spec (interest arg)The month's par curve (+ spread) as a curve spec. Caution: par yields, not spots — discounting on it is the labeled "Treasury + spread" approximation convention; for spot discounting use aos_HqmCurve. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; AI1, 2026-09-24, R7's rule carried to this family).
aos_TreasuryTable(year, month, [header], [spreadBps])Treasury par curve, spillingSpills Maturity, Par yield for every published tenor of the month (tenors vary — a 1.5-mo tenor appears 2025-02). Each row == aos_Treasury at that maturity, spread included. An unpublished year-month REFUSES — the cell shows #AOS: no … curve is published for YYYY-MM, naming the months on file (it used to price on the latest month; AI1, 2026-09-24, R7's rule carried to this family).
aos_PbgcPremium(year, "flat"|"vrp"|"cap"|"me")PBGC premium ratesSingle-employer flat $/participant, variable-rate $/\$1,000 UVB, VRP per-participant cap $, or multiemployer flat $/participant. A year not on file REFUSES (#AOS: no PBGC … is published for YYYY, naming the years on file — 2026-09-24, N8; it used to clamp to the nearest year): a premium rate is set for each plan year and is not projectable. Attested input — verify.
aos_PbgcMaxGuarantee(year, [age])PBGC §4022(b) max guaranteeStraight-life max guaranteeable benefit, $/month; age defaults 65, and ages 45–75 apply PBGC's published §4022 age factors (interpolated; 66 = 1.10 … 70 = 1.66 … 75 = 3.04 — 2026-09-25, AP3 / Q8: ages above 65 used to return the age-65 maximum, $7,789.77 at 70 in 2026 where PBGC publishes $12,931.02). An age outside 45–75 REFUSES (#AOS: no … age factor is published for age …) — PBGC prints no row there (it took the age-45 factor below 45, a row PBGC does not print; Scott's ruling Q8). A year not on file REFUSES (#AOS: …, naming the years on file — 2026-09-24, N8; it used to clamp to the nearest year): the maximum guarantee is published for each year and is not projectable. Attested input — verify.
aos_SsBendPoint(year, 1|2)Social Security PIA bend pointsThe first/second dollar bend point in the PIA formula (90%/32%/15%). Backfilled from the source of record (Q6, 2026-09-25 — every cell rebuilt from the committed page by ssDataProvenance.test.js): eligibility years 1979–2026 (SSA OACT bend points). A year outside them REFUSES (#AOS: no Social Security PIA bend point is published for …) — it was held flat at the nearest stored year. Attested input — verify vs SSA.
aos_SsCola(year)Social Security COLA (decimal)The CPI-W–based benefit increase effective that January (SSA's row for the prior year, payable from its December). Backfilled from the source of record (Q6, 2026-09-25 — every cell rebuilt from the committed page by ssDataProvenance.test.js): 1984–2026 (SSA OACT COLA series; the 1975–82 COLAs were payable for June and are not January COLAs). A year outside them REFUSES (#AOS: …) — a future COLA is an assumption the plan states, not a lookup; it was held flat. Attested input — verify vs SSA.
aos_SsAwi(year)National Average Wage IndexThe SSA AWI ($); ~2-year publication lag. Backfilled from the source of record (Q6, 2026-09-25 — every cell rebuilt from the committed page by ssDataProvenance.test.js): 1951–2024 (SSA OACT AWI series). A year outside them REFUSES (#AOS: …) — a later year's index is a wage-growth assumption; it was held flat. Attested input — verify vs SSA.
aos_SsNRA(birthYear, [output])Social Security full (normal) retirement ageThe statutory §416(l) table (not a published series): 65 through 1937, +2 mo/yr to 66 (1943–54), +2 mo/yr to 67 (1960+). output = "years" (default, decimal e.g. 66.8333) | "months" | "ym" ("66y 10m"). SSA treats a Jan-1 birth as the prior year — pass the adjusted year if that applies.
aos_SsPIA(aime, eligibilityYear, [colaToYear])Social Security PIA from AIME90/32/15 bend formula on the eligibility-year (age-62) bend points, dime-rounded; optional COLA-forward. Supply AIME. An eligibility year outside the bend-point series, or a `colaToYear` past the last published COLA, REFUSES (#AOS: …; Q6, 2026-09-25) — it held the nearest stored year flat and compounded the last COLA forward, an unstated COLA assumption. Eligibility years 1979–1982 cannot be COLA-forwarded: they answer their own PIA, and any colaToYear after the eligibility year REFUSES — the June 1975–82 COLAs are not January COLAs, so the stored COLA series starts at 1984 (2026-09-25, AT5); 1983 forwards. Attested input — verify vs SSA.
aos_Cpi(year)BLS CPI-U annual averageAll Urban Consumers, U.S. city average (1982-84=100); for plan COLAs tied to CPI-U. (SS COLA = CPI-W-based: aos_SsCola.) Backfilled from the source of record (Q6, 2026-09-25 — every cell rebuilt from the committed page by ssDataProvenance.test.js): 1913–2025 (BLS series CUUR0000SA0, the M13 annual average). A year outside them REFUSES (#AOS: …) — it was held flat. Attested input — verify vs BLS.

Dates

FunctionUse it forMethod / anchor
aos_DateAdd(date, years, months, days, [eom], [method])Add y/m/d (eom-aware); service adjustmentmethod shifts an entry date FORWARD by a break, quantized to whole months/years (down/up/near) for credited service. Default "exact". Break-in-service eligibility is the user's.
aos_DateDiff(start, end, [interval], [rounding])Difference in y/m/d, with rounding"y"/none == ActOS ageAtDate. rounding="component" returns the calendar component; interval "ymd" returns the "Xy Ym Zd" breakdown string.

Diagnostics

FunctionUse it forMethod / anchor
aos_Version()Version stamp (audit)A one-line stamp: the add-in version, the ActOS ENGINE_VERSION these numbers reproduce, the data-library version, and the generation date. Put it next to a signed result.
aos_License()License statusThe beta key / tier (metadata only — never plan data). During the beta the add-in always runs (fail-open); set your beta key via the ribbon License button.
aos_Versions()Full version block (spills)Spills component, value rows — add-in, engine, data manifest, mortality-table count, generation date. The audit stamp for a signed workbook.
aos_Attest(table)Assumptions behind a calcFor a mortality table id: its basis/gender/base year, the data-provenance caveat, and the engine + data + generation stamp — a one-cell attestation to drop beside a signed number. Omit table for the engine/data stamp only.
aos_Tables([contains])Discover table ids (spills)Lists the available mortality table ids, optionally filtered to those containing a substring. Copy a name into any table argument — the fix for #NAME? from a mis-typed table.
aos_Scales()Discover improvement-scale ids (spills)Lists the scale ids (incl. "None") valid for the scale argument.
aos_Check(table)Validate a table id"OK" if the table exists, else an actionable #AOS message naming the nearest valid table (edit-distance "did you mean…?"). Run it to debug a #NAME? before signing.
aos_IsRefused(cell)Is this cell a refusal? (test before totalling)TRUE when the cell holds an ActuarialOS typed refusal — text starting #AOS: — else FALSE (a number, other text, blank, an Excel error value). A refusal is TEXT by ruling (2026-09-25, Q10 (i) — its sentence is the point), and SUM / AVERAGE skip text while ISERROR reads it FALSE, so a total over refused cells drops them without a sign. A range answers per cell (spills): =SUMPRODUCT(--aos_IsRefused(B2:B99)) counts what a SUM beside it skipped. Never refuses.
aos_Default(value, fallback)Reproducible firm defaultReturns value when supplied, else fallback. Point every formula at one firm-config cell with a built-in fallback — e.g. aos_Default(Config!$B$1, "Pri-2012 Male"). The resolved value stays visible in the cell; there is no hidden global default state (a deliberate choice — silent machine-level defaults would let the same formula give different numbers, breaking reproducibility).

Verify before signing

Functions reproduce actuarialEngine.js exactly; the data is the attest surface.

See PROJECT.md (§7 limitations, §8 findings) for the full conventions and caveats.


Glossary (concepts behind the functions)

For the new analyst. The same glossary (plus a "Your first annuity" walkthrough) is in the ribbon Help dialog; a full worked workbook is Addin/SAMPLES.md.

← ActuarialOS for Excel · Questions: hello@actuarialos.io