Once the agent was live on top of the semantic layer I wrote about last time, the next question was obvious: where is enrollment heading? Once a tool can tell you how many students were enrolled last fall, the very next question anyone asks is how many there'll be next fall. And an agent with SQL access and a code tool will cheerfully answer, because fitting a line through some numbers is easy. The hard part of forecasting was never the arithmetic.
Agents in this setup can be given skills, which are short written guides the agent loads when a question matches what the skill is for. So instead of hoping the agent would forecast sensibly on its own, I wrote one for enrollment projection. Reading it back, almost all of it is a list of things not to do. I think that's the correct shape for a forecasting skill, and I think it's the most useful thing about the whole exercise, because it's the same list I'd give a new analyst.
The first rule is about the most recent data point, which is the one an agent most wants to use. The term that's currently in progress is not a data point yet. Until the official census freeze, students are still registering and dropping, and the headcount moves around. I measured this directly: the same query against the in-progress fall term, run on two builds a few hours apart, moved by about four and a half percent with no change to the query at all. If you anchor a projection on that number, you inherit its error and then multiply it forward. So the skill says to find the latest term whose census has actually frozen, fit only on frozen terms, and report the in-progress term separately with a label saying it's provisional.
| Build A, morning | Build B, afternoon | |
|---|---|---|
| Last frozen fall | 152.0 | 152.0 |
| Fall in progress | 158.4 | 151.3 |
Made-up index values, real behavior. The frozen term doesn't move between builds. The live one moved about four and a half percent with the same query.
The second rule is about seasons. Terms aren't interchangeable. A fall term is far bigger than a summer term, and summer is actually several different kinds of summer, each with a different population. A trend fitted across all of them measures the academic calendar, not enrollment. So the skill only ever compares like with like: fall against prior falls, spring against prior springs, year over year. And it warns about a break in the series, because the university recently collapsed three summer sessions into one, which means any summer trend now compares two different things across that line. That's the same term consolidation that deleted a whole summer from the data in my first post about the unified record. It keeps showing up.
The third rule is the one I care about most: the answer is a range, not a number. The method is deliberately naive. Take the year-over-year growth rates for the last five frozen terms of the same type, and project forward using the smallest and the largest of them. The spread between those two is the honest uncertainty, and it's wide. One of those years is a rebound from the pandemic, which the skill tells the agent to name explicitly, because a post-pandemic bounce is not a growth rate anyone should plan around. And the horizon is capped at two years, because past that the range gets wider than the answer is useful.
def project_range(frozen_falls: list[float], years_ahead: int = 2):
"""Naive range from the slowest and fastest recent year-over-year growth.
frozen_falls: oldest to newest, census-frozen terms only."""
recent = frozen_falls[-6:] # five year-over-year changes
growth = [b / a - 1 for a, b in zip(recent, recent[1:])]
lo, hi = min(growth), max(growth)
last = frozen_falls[-1]
return [
(round(last * (1 + lo) ** n), round(last * (1 + hi) ** n))
for n in range(1, min(years_ahead, 2) + 1)
]
project_range([100, 117, 127, 137, 141, 152])
# -> [(156, 178), (161, 208)] a wide range, and honestly so
Made-up index (2020 = 100). The band runs from the slowest to the fastest recent year-over-year growth. The hollow point is the fall still in progress, shown but never used to fit anything.
The naivety is on purpose. There are proper time series methods, and a model with seasonal terms and confidence intervals would look far more serious. But the agent's audience isn't forecasters. It's people who want a sense of direction and need to understand where it came from. "Between this and this, based on how much the last five falls grew" is something anyone can check with a calculator. A confidence interval from a model they can't see is something they'll quote without the caveats.
Then come the refusals, and they're the part I'd defend hardest. The agent must never project a graduation, completion or retention rate. Those numbers come from a table that deliberately only includes cohorts old enough to have finished, because the recent ones don't have an outcome yet. Projecting them forward reintroduces the exact bias that design was built to remove. So when someone asks what next year's graduation rate will be, the agent says it can't be done with this data, and explains why. It also has to say when it's slicing: a projection for one college or one demographic group is far noisier than for the whole university, because small groups move for reasons the total doesn't, so the skill makes the agent say the uncertainty is much wider. And it must never present an extrapolation as a plan or a target. It has no idea about admissions goals, capacity, new programs, or policy changes. Anyone actually planning on a number needs the enrollment management office, not a chatbot.
Every answer has to state which terms it used, that the in-progress term was left out, the range and where it came from, and that this is a naive extrapolation rather than an institutional projection. That's a lot of caveats for one answer. It's also, word for word, what I'd expect a careful human analyst to say.
That's what I keep coming back to with this kind of work. Writing a skill for an agent is really writing down judgment. The agent is perfectly capable of the math. What it doesn't have is the context that makes some math inappropriate: that the current term is still moving, that summers changed shape, that one year was a pandemic, that a censored table can't be projected. Those are things a data engineer knows because they've been burned by each one. Putting them in a file where the agent reads them every time is the closest thing I've found to making that experience transferable.
The honest caveat on my own work here matches the one from the semantic layer post. The skill is written and attached. What I haven't done yet is watch a lot of real people push on it with questions I didn't anticipate. That's where the next set of rules will come from.