Master enterprise reporting automation including scheduling, distribution, templates, governance, and monitoring
Master enterprise report automation including scheduling, distribution management, template design, governance, and operational monitoring.
# Basic automated report setup
report:
name: "Daily Sales Summary"
schedule: "0 7 * * 1-5" # Weekdays at 7 AM
recipients:
- sales-team@company.com
format: pdf
data_refresh: before_send
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā REPORT LIFECYCLE ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā 1. SCHEDULE Cron expression triggers ā
ā ā ā
ā 2. DATA REFRESH Update underlying data ā
ā ā ā
ā 3. RENDER Generate report output ā
ā ā ā
ā 4. VALIDATE Check for errors/empty data ā
ā ā ā
ā 5. DISTRIBUTE Send to recipients ā
ā ā ā
ā 6. ARCHIVE Store for compliance/history ā
ā ā ā
ā 7. MONITOR Track delivery & engagement ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
CRON EXPRESSION FORMAT
āāāāāāāāāāāāāā minute (0-59)
ā āāāāāāāāāāāāāā hour (0-23)
ā ā āāāāāāāāāāāāāā day of month (1-31)
ā ā ā āāāāāāāāāāāāāā month (1-12)
ā ā ā ā āāāāāāāāāāāāāā day of week (0-6, 0=Sunday)
ā ā ā ā ā
* * * * *
EXAMPLES:
0 7 * * 1-5 ā Weekdays at 7:00 AM
0 8 * * 1 ā Every Monday at 8:00 AM
0 9 1 * * ā First of every month at 9:00 AM
30 6 * * * ā Every day at 6:30 AM
0 */4 * * * ā Every 4 hours
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā DISTRIBUTION CHANNELS ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā EMAIL ā
ā ⢠Attachment (PDF, Excel, PowerPoint) ā
ā ⢠Inline HTML body ā
ā ⢠Link to portal ā
ā ā
ā FILE DROP ā
ā ⢠SharePoint/Teams ā
ā ⢠Network share ā
ā ⢠Cloud storage (S3, Azure Blob, GCS) ā
ā ā
ā API/WEBHOOK ā
ā ⢠Push to external system ā
ā ⢠Trigger downstream process ā
ā ⢠Slack/Teams notification ā
ā ā
ā PORTAL ā
ā ⢠Self-service access ā
ā ⢠Interactive exploration ā
ā ⢠Subscription management ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
schedules:
daily_sales:
name: "Daily Sales Report"
cron: "0 7 * * 1-5"
timezone: "America/New_York"
enabled: true
data_refresh:
type: "incremental"
timeout_minutes: 30
retry_on_failure: true
render:
format: "pdf"
template: "executive_summary"
page_size: "letter"
orientation: "landscape"
distribution:
method: "email"
recipients:
static:
- "sales-leadership@company.com"
dynamic:
query: "SELECT email FROM users WHERE role = 'regional_manager'"
email:
subject: "Daily Sales Report - {{date}}"
body_template: "templates/email/daily_sales.html"
from: "reports@company.com"
validation:
rules:
- type: "row_count"
minimum: 1
action: "skip_and_notify"
- type: "data_freshness"
max_age_hours: 24
action: "warn"
archive:
enabled: true
destination: "s3://reports-archive/sales/"
retention_days: 365
monitoring:
alert_on_failure: true
alert_recipients: ["ops-team@company.com"]
sla_minutes: 60
<!DOCTYPE html>
<html>
<head>
<style>
.kpi-card {
display: inline-block;
padding: 20px;
margin: 10px;
background: #f8f9fa;
border-radius: 8px;
text-align: center;
}
.kpi-value {
font-size: 32px;
font-weight: bold;
color: #2563eb;
}
.kpi-label {
font-size: 14px;
color: #6b7280;
}
.trend-up { color: #22c55e; }
.trend-down { color: #ef4444; }
</style>
</head>
<body>
<h1>Daily Sales Report - {{date}}</h1>
<div class="kpi-section">
<div class="kpi-card">
<div class="kpi-value">{{revenue | currency}}</div>
<div class="kpi-label">Revenue</div>
<div class="{{revenue_trend_class}}">{{revenue_trend}}%</div>
</div>
<div class="kpi-card">
<div class="kpi-value">{{orders | number}}</div>
<div class="kpi-label">Orders</div>
<div class="{{orders_trend_class}}">{{orders_trend}}%</div>
</div>
<div class="kpi-card">
<div class="kpi-value">{{aov | currency}}</div>
<div class="kpi-label">Avg Order Value</div>
<div class="{{aov_trend_class}}">{{aov_trend}}%</div>
</div>
</div>
<h2>Highlights</h2>
<ul>
{{#each highlights}}
<li>{{this}}</li>
{{/each}}
</ul>
<p>
<a href="{{dashboard_url}}">View Full Dashboard</a>
</p>
<footer>
<p style="color: #9ca3af; font-size: 12px;">
This report was automatically generated on {{generated_at}}.
{{#if confidential}}CONFIDENTIAL - Internal Use Only{{/if}}
</p>
</footer>
</body>
</html>
from datetime import datetime
from typing import List, Dict
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import encoders
class ReportDistributor:
def __init__(self, config: Dict):
self.config = config
self.smtp_server = config['smtp_server']
self.smtp_port = config['smtp_port']
def send_report(
self,
recipients: List[str],
subject: str,
body_html: str,
attachments: List[str] = None
) -> Dict:
"""Send report via email with attachments."""
results = {"sent": [], "failed": []}
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = self.config['from_address']
# Add HTML body
msg.attach(MIMEText(body_html, 'html'))
# Add attachments
if attachments:
for filepath in attachments:
with open(filepath, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header(
'Content-Disposition',
f'attachment; filename="{filepath.split("/")[-1]}"'
)
msg.attach(part)
# Send to each recipient
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
server.starttls()
server.login(self.config['username'], self.config['password'])
for recipient in recipients:
try:
msg['To'] = recipient
server.send_message(msg)
results["sent"].append(recipient)
except Exception as e:
results["failed"].append({
"recipient": recipient,
"error": str(e)
})
return results
-- Report delivery metrics
WITH delivery_stats AS (
SELECT
report_name,
DATE_TRUNC('day', scheduled_time) AS date,
COUNT(*) AS total_runs,
SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) AS successful,
SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failed,
AVG(duration_seconds) AS avg_duration,
MAX(duration_seconds) AS max_duration
FROM report_execution_log
WHERE scheduled_time >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY report_name, DATE_TRUNC('day', scheduled_time)
)
SELECT
report_name,
date,
total_runs,
successful,
failed,
ROUND(successful::DECIMAL / NULLIF(total_runs, 0) * 100, 2) AS success_rate,
ROUND(avg_duration, 1) AS avg_duration_sec,
max_duration AS max_duration_sec
FROM delivery_stats
ORDER BY date DESC, report_name;
guidelines:
- Consider timezone of recipients
- Avoid peak hours for data refresh
- Stagger reports to avoid resource contention
- Build in buffer time before meetings
- Use business day calendars for business reports
example_timing:
executive_reports:
timing: "7:00 AM local time"
reason: "Before morning standup"
operational_reports:
timing: "6:00 AM local time"
reason: "Before shift starts"
financial_reports:
timing: "After month-end close + 2 days"
reason: "Data completeness"
data_classification:
public:
distribution: "unrestricted"
watermark: false
encryption: false
internal:
distribution: "employees_only"
watermark: true
encryption: "in_transit"
confidential:
distribution: "named_recipients"
watermark: true
encryption: "at_rest_and_transit"
drm: true
audit_required: true
restricted:
distribution: "approval_required"
watermark: true
encryption: "at_rest_and_transit"
drm: true
audit_required: true
no_download: true
retention_policy:
operational: "90 days"
analytical: "2 years"
regulatory: "7 years"
legal_hold: "indefinite"
class ReportErrorHandler:
def handle_error(self, error: Exception, context: dict) -> dict:
error_type = type(error).__name__
strategies = {
'DataRefreshTimeout': {
'action': 'retry',
'max_retries': 3,
'backoff': 'exponential',
'notify': 'ops_team'
},
'NoDataAvailable': {
'action': 'skip',
'notify': 'report_owner',
'message': 'Report skipped - no data for period'
},
'RecipientInvalid': {
'action': 'skip_recipient',
'notify': 'admin',
'continue': True
},
'RenderFailure': {
'action': 'retry_then_escalate',
'max_retries': 2,
'notify': 'dev_team'
}
}
return strategies.get(error_type, {
'action': 'escalate',
'notify': 'on_call'
})
-- Query to generate recipient list based on data
SELECT DISTINCT
u.email,
u.first_name,
u.region,
'regional_manager' AS role
FROM users u
INNER JOIN sales_data s ON u.region = s.region
WHERE u.role = 'regional_manager'
AND u.is_active = 1
AND s.report_date = CURRENT_DATE - 1
AND s.sales_amount > 0;
report_template:
name: "Regional Sales Report"
parameters:
- name: region
type: string
source: "dynamic"
query: "SELECT DISTINCT region FROM dim_region WHERE is_active = 1"
- name: date_range
type: date_range
default: "last_month"
- name: include_forecast
type: boolean
default: false
personalization:
- Filter data by recipient's region
- Include recipient's name in greeting
- Highlight recipient's team performance
# Send personalized version to each recipient
def burst_report(report_template, recipients, data):
for recipient in recipients:
# Filter data for this recipient
filtered_data = filter_data_for_recipient(data, recipient)
# Generate personalized report
report = render_report(
template=report_template,
data=filtered_data,
recipient=recipient
)
# Send individual report
send_report(
to=recipient.email,
report=report,
subject=f"Your {report_template.name} - {recipient.region}"
)
const executeWithRetry = async (
operation: () => Promise<any>,
config: RetryConfig
) => {
const { maxRetries, backoffMs, retryableErrors } = config;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
const isRetryable = retryableErrors.includes(error.code);
const hasRetriesLeft = attempt < maxRetries;
if (!isRetryable || !hasRetriesLeft) {
throw error;
}
console.log(`Attempt ${attempt + 1} failed, retrying in ${backoffMs[attempt]}ms`);
await sleep(backoffMs[attempt]);
}
}
};
// Usage
await executeWithRetry(
() => sendReport(report),
{
maxRetries: 3,
backoffMs: [1000, 5000, 15000],
retryableErrors: ['SMTP_TIMEOUT', 'RATE_LIMITED', 'TEMPORARY_FAILURE']
}
);
const reportHooks = {
onScheduleTrigger: (reportName: string) => {
console.log(`[REPORT] Triggered: ${reportName}`);
metrics.increment('reports.triggered', { report: reportName });
},
onDataRefresh: (reportName: string, duration: number) => {
console.log(`[REPORT] Data refreshed for ${reportName} in ${duration}s`);
metrics.histogram('reports.refresh_duration', duration);
},
onDeliverySuccess: (reportName: string, recipient: string) => {
console.log(`[REPORT] Delivered ${reportName} to ${recipient}`);
metrics.increment('reports.delivered');
},
onDeliveryFailure: (reportName: string, error: Error) => {
console.error(`[REPORT] Failed: ${reportName} - ${error.message}`);
metrics.increment('reports.failed');
alerting.notify('report_failure', { report: reportName, error });
}
};
describe('Report Automation Skill', () => {
describe('Scheduling', () => {
it('should parse cron expression correctly', () => {
const schedule = parseCron('0 7 * * 1-5');
expect(schedule.nextRun().getHours()).toBe(7);
expect([1, 2, 3, 4, 5]).toContain(schedule.nextRun().getDay());
});
});
describe('Distribution', () => {
it('should send to all valid recipients', async () => {
const result = await distributor.send({
recipients: ['valid@example.com'],
report: mockReport
});
expect(result.sent).toHaveLength(1);
expect(result.failed).toHaveLength(0);
});
it('should handle invalid recipients gracefully', async () => {
const result = await distributor.send({
recipients: ['invalid@', 'valid@example.com'],
report: mockReport
});
expect(result.sent).toHaveLength(1);
expect(result.failed).toHaveLength(1);
});
});
describe('Validation', () => {
it('should skip empty reports when configured', async () => {
const result = await validator.validate({
report: emptyReport,
rules: [{ type: 'row_count', minimum: 1, action: 'skip' }]
});
expect(result.shouldSend).toBe(false);
});
});
});
| Issue | Cause | Solution |
|---|---|---|
| Report not sent | Schedule misconfigured | Verify cron and timezone |
| Empty report | Data not refreshed | Check refresh dependencies |
| Slow delivery | Large attachment | Compress or send link |
| Recipient not receiving | Email filtering | Whitelist sender domain |
| Stale data | Refresh timeout | Increase timeout, optimize query |
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2024-01 | Initial release |
| 2.0.0 | 2025-01 | Production-grade with governance |