Replacing Manual Month-End Close Reporting with an AI Agent
Month-end close is one of the most labor-intensive rituals in any finance team's calendar. Data analysts spend days pulling figures from ERPs, reconciling discrepancies across systems, and formatting reports that executives will read in five minutes. The underlying work is predictable, rule-based, and repeatable — the exact profile for an AI agent to take over.
This post walks through how to build a monthly close reporting agent that handles the full cycle: data extraction, reconciliation, anomaly flagging, and narrative generation.
What the Agent Replaces
A typical monthly close reporting workflow looks like this:
- Export GL data from NetSuite or SAP
- Pull actuals from the data warehouse (Snowflake, Redshift, or BigQuery)
- Reconcile the two — flag differences above a materiality threshold
- Compare actuals to budget and prior period
- Write variance commentary ("Revenue was $2.3M above budget driven by...")
- Format into a board/CFO deck template
- Email to stakeholders
Steps 1–6 are fully automatable. Step 7 is a one-line script.
Architecture
┌─────────────────────────────────────────────────────┐
│ CLOSE REPORTING AGENT │
│ │
│ 1. Extract GL actuals via ERP MCP │
│ 2. Pull budget & prior period via Warehouse MCP │
│ 3. Reconcile → flag variances above threshold │
│ 4. Generate narrative via Claude / OpenAI │
│ 5. Write report to Filesystem MCP │
│ 6. Deliver via Notification MCP (email / Slack) │
└─────────────────────────────────────────────────────┘
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌───▼──────────┐
│ ERP MCP │ │ Warehouse│ │ Notification │
│ (NetSuite │ │ MCP │ │ MCP │
│ / SAP) │ │(Snowflake)│ │(Slack/Email) │
└─────┬─────┘ └─────┬─────┘ └───────────────┘
│ │
┌─────▼──────────────▼──────┐
│ Data Reconciliation │
│ & Variance Engine │
└───────────────────────────┘
MCP Configuration
{
"mcpServers": {
"warehouse": {
"command": "uvx",
"args": ["mcp-server-snowflake"],
"env": {
"SNOWFLAKE_ACCOUNT": "${SF_ACCOUNT}",
"SNOWFLAKE_DATABASE": "FINANCE_DW",
"SNOWFLAKE_WAREHOUSE": "REPORTING_WH"
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/reports/monthly-close"]
},
"notifications": {
"command": "uvx",
"args": ["mcp-server-slack"],
"env": {
"SLACK_BOT_TOKEN": "${SLACK_TOKEN}"
}
}
}
}
The Reconciliation Logic
The agent runs a structured reconciliation check before generating any narrative. This is non-negotiable — a narrative built on unreconciled data is worse than no narrative at all.
RECONCILIATION_PROMPT = """
You have access to two data sources:
- warehouse: contains actuals from our data warehouse (source of truth for management reporting)
- A GL export will be provided as structured JSON in this message
Reconciliation rules:
1. For each P&L line item, compare warehouse actuals to GL actuals
2. Flag any difference greater than $10,000 OR 2% of the line item value (whichever is smaller)
3. Do NOT proceed to narrative generation if total unreconciled variance exceeds $50,000
4. Output a reconciliation_status: "CLEAN" | "FLAGGED" | "BLOCKED"
If BLOCKED, output only the list of unreconciled items and stop.
"""
By making the agent block on unreconciled data above a materiality threshold, you preserve the integrity that finance teams depend on.
Variance Commentary Generation
Once reconciliation is clean, the agent generates variance commentary using the same structure your finance team already uses:
System prompt excerpt:
For each major P&L category where actual vs budget variance exceeds 5%:
- State the variance in dollars and percentage
- Identify the top 1-2 drivers (query the warehouse for supporting detail if needed)
- Use past tense, factual language — no hedging phrases like "it appears" or "may have been"
- Format: "[Metric] was [above/below] budget by $[X]M ([Y]%), primarily driven by [driver]."
Example output:
"Gross margin was above budget by $1.2M (3.1%), primarily driven by favorable
raw material costs in the EMEA region and a mix shift toward higher-margin
enterprise contracts."
Scheduling and Delivery
# Airflow DAG — runs on the 2nd business day of each month
from airflow.decorators import dag, task
from airflow.timetables.events import EventsTimetable
from datetime import datetime
@dag(
schedule="0 6 2-5 * *", # 6am on days 2-5 of each month
start_date=datetime(2025, 9, 1),
catchup=False,
tags=["finance", "close-reporting"]
)
def monthly_close_agent():
@task
def run_reconciliation():
# Run agent in reconciliation-only mode
return agent.run(mode="reconcile")
@task
def run_narrative(recon_result):
if recon_result["status"] == "BLOCKED":
notify_finance_team(recon_result["flags"])
return
agent.run(mode="narrative", recon_data=recon_result)
recon = run_reconciliation()
run_narrative(recon)
monthly_close_agent()
The DAG runs on days 2–5 of each month so it catches whatever day the GL is available. If reconciliation is blocked, it notifies the finance team with the specific flagged items rather than failing silently.
What Changes for the Finance Team
- Eliminated: pulling and joining data across systems manually
- Eliminated: formatting variance tables in Excel
- Eliminated: writing first-draft commentary from scratch
- Retained: finance team review and sign-off before distribution
- Retained: judgment calls on non-standard items the agent flags
The agent produces a draft that a finance professional reviews in 20 minutes instead of building from scratch over two days. The human stays in the loop where judgment matters; the machine handles the repeatable extraction and synthesis.
Book a strategy session to design your close reporting agent.