Use Cases
Automated Project Reports
Schedule weekly status reports generated by querying your document vault with the chat API, delivered to Slack or email.
What you will build
A reporting script that queries an Alloovium project with a set of predefined questions, assembles the answers into a formatted report, and delivers it to a Slack channel or email address on a schedule. No infrastructure beyond a scheduled process (cron, GitHub Actions, or a cloud scheduler) is required.
Difficulty and build time
scopes.Prerequisites
- An Alloovium API key with
scopesscopes. - Python 3.11+ with
depinstalled. - An existing Alloovium project with processed documents — at minimum a contract and a progress report.
- A Slack incoming webhook URL, or SMTP credentials, for delivery.
Step 1 — Set up a project vault
If you do not already have a project, create one and upload the source documents. The report will draw its answers from these documents, so upload the contract, progress reports, meeting minutes, and any other documents that contain status information.
Refer to Contract Intelligence — Step 1 for the full upload flow. For the reporting use case, uploading documents through the Alloovium web UI is fine — there is no requirement to automate the upload for this guide.
bash# Quick check — list your projects and find the one to report on curl -sS "https://api.alloovium.com/api/v1/projects?limit=20" \ -H "Authorization: Bearer $ALLOOVIUM_API_KEY" | python3 -m json.tool
Step 2 — Write a report template
Define the questions that make up your report as a structured template. A template is just a list of question objects with a section name and a prompt. This makes it easy to maintain and version the report structure independently from the delivery code.
pythonfrom dataclasses import dataclass @dataclass class ReportQuestion: section: str question: str format_hint: str = "" # optional hint to shape the answer WEEKLY_REPORT_TEMPLATE: list[ReportQuestion] = [ ReportQuestion( section="Progress Summary", question="Summarise the overall project progress this week based on the latest reports.", format_hint="2-3 sentences.", ), ReportQuestion( section="Milestones", question=( "Which project milestones have been achieved and which are at risk? " "Include any completion dates mentioned." ), format_hint="Bullet list.", ), ReportQuestion( section="Open RFIs", question=( "List any open RFIs or requests for information from the current documents. " "Include the RFI number and subject if available." ), format_hint="Bullet list, or 'None outstanding' if clear.", ), ReportQuestion( section="Payment Status", question=( "What is the current payment claim status? " "Include the claim number and amount if stated." ), format_hint="1-2 sentences.", ), ReportQuestion( section="Key Risks", question="What are the top three risks to programme or cost mentioned in the documents?", format_hint="Numbered list of three items.", ), ReportQuestion( section="Actions Required", question=( "List any outstanding actions or decisions required from the principal or contractor " "mentioned in the documents." ), format_hint="Bullet list, or 'None identified' if clear.", ), ]
Step 3 — Call the chat API with each question
Create a single conversation per report run and send each question in sequence. Using one conversation keeps the context window consistent — the LLM has access to all prior answers when answering later questions, which improves coherence.
pythonimport httpx import json BASE_URL = "https://api.alloovium.com/api/v1" API_KEY = "ak_live_YOUR_KEY_HERE" HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} async def ask_and_collect( client: httpx.AsyncClient, conversation_id: str, question: str, ) -> str: """Stream a single chat response and return the full answer.""" full: list[str] = [] async with client.stream( "POST", f"{BASE_URL}/conversations/{conversation_id}/messages", json={"content": question}, headers={**HEADERS, "Accept": "text/event-stream"}, timeout=120.0, ) as stream: async for line in stream.aiter_lines(): if not line.startswith("data:"): continue payload = line[5:].strip() if payload in ("", "[DONE]"): continue try: event = json.loads(payload) except json.JSONDecodeError: continue if event.get("type") == "token": full.append(event.get("token", "")) elif event.get("type") == "complete": return event.get("answer", "".join(full)) return "".join(full) async def generate_report_answers( project_id: str, template: list[ReportQuestion], ) -> list[tuple[ReportQuestion, str]]: """Run all questions and return (question, answer) pairs.""" results: list[tuple[ReportQuestion, str]] = [] async with httpx.AsyncClient(timeout=180.0) as client: conv_resp = await client.post( f"{BASE_URL}/conversations", json={"project_id": project_id, "title": "Weekly report"}, headers=HEADERS, ) conv_resp.raise_for_status() conv_id = conv_resp.json()["id"] for q in template: prompt = q.question if q.format_hint: prompt += f"\n\nFormat: {q.format_hint}" answer = await ask_and_collect(client, conv_id, prompt) results.append((q, answer)) return results
Step 4 — Assemble the report
Take the list of answered questions and format them into a readable report. The example below produces both a plain-text version (for email) and a Slack-flavoured markdown version.
pythonfrom datetime import date def assemble_report( project_name: str, answers: list[tuple[ReportQuestion, str]], ) -> tuple[str, str]: """Return (plain_text, slack_markdown) versions of the report.""" today = date.today().strftime("%d %B %Y") header = f"Weekly Project Report — {project_name} — {today}" plain_lines = [header, "=" * len(header), ""] slack_lines = [f"*{header}*", ""] for q, answer in answers: plain_lines.append(f"{q.section}") plain_lines.append("-" * len(q.section)) plain_lines.append(answer.strip()) plain_lines.append("") slack_lines.append(f"*{q.section}*") slack_lines.append(answer.strip()) slack_lines.append("") return "\n".join(plain_lines), "\n".join(slack_lines)
Step 5 — Deliver via Slack or email
Deliver to Slack
Post to a Slack incoming webhook. Create a webhook in your Slack workspace under path and set the URL in your environment.
pythonimport os async def deliver_to_slack(slack_markdown: str) -> None: """Post the report to a Slack channel via incoming webhook.""" webhook_url = os.environ["SLACK_WEBHOOK_URL"] async with httpx.AsyncClient() as client: resp = await client.post( webhook_url, json={"text": slack_markdown}, headers={"Content-Type": "application/json"}, ) resp.raise_for_status()
Deliver via email
Use Python's built-in lib to send the plain-text report. For HTML-formatted emails, wrap the sections in simple HTML instead.
pythonimport smtplib import os from email.message import EmailMessage def deliver_by_email( plain_text: str, subject: str, to_addresses: list[str], ) -> None: """Send the plain-text report via SMTP.""" msg = EmailMessage() msg["Subject"] = subject msg["From"] = os.environ["SMTP_FROM"] msg["To"] = ", ".join(to_addresses) msg.set_content(plain_text) with smtplib.SMTP_SSL( os.environ["SMTP_HOST"], int(os.environ.get("SMTP_PORT", "465")), ) as smtp: smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"]) smtp.send_message(msg)
Step 6 — Cron job setup
Wire everything together in a single entry-point script and schedule it with cron, GitHub Actions, or a cloud scheduler.
Entry-point script
python# weekly_report.py import asyncio import os PROJECT_ID = os.environ["ALLOOVIUM_PROJECT_ID"] PROJECT_NAME = os.environ.get("PROJECT_NAME", "Project") REPORT_EMAIL_TO = os.environ.get("REPORT_EMAIL_TO", "").split(",") async def main() -> None: answers = await generate_report_answers(PROJECT_ID, WEEKLY_REPORT_TEMPLATE) plain_text, slack_md = assemble_report(PROJECT_NAME, answers) if os.environ.get("SLACK_WEBHOOK_URL"): await deliver_to_slack(slack_md) if REPORT_EMAIL_TO and REPORT_EMAIL_TO[0]: from datetime import date subject = f"Weekly Report: {PROJECT_NAME} — {date.today():%d %b %Y}" deliver_by_email(plain_text, subject, REPORT_EMAIL_TO) print("Report delivered successfully.") if __name__ == "__main__": asyncio.run(main())
Linux/macOS cron (every Monday at 07:00)
bash# Add to crontab with: crontab -e 0 7 * * 1 cd /opt/reports && /usr/bin/python3 weekly_report.py >> /var/log/weekly_report.log 2>&1
GitHub Actions (every Monday at 07:00 UTC)
yaml# .github/workflows/weekly-report.yml name: Weekly Project Report on: schedule: - cron: '0 7 * * 1' workflow_dispatch: # allow manual runs jobs: report: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - run: pip install httpx - run: python weekly_report.py env: ALLOOVIUM_API_KEY: ${{ secrets.ALLOOVIUM_API_KEY }} ALLOOVIUM_PROJECT_ID: ${{ secrets.ALLOOVIUM_PROJECT_ID }} PROJECT_NAME: "City Road Upgrade" SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} REPORT_EMAIL_TO: ${{ secrets.REPORT_EMAIL_TO }} SMTP_HOST: smtp.example.com SMTP_FROM: reports@example.com SMTP_USER: ${{ secrets.SMTP_USER }} SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
Keep secrets out of source control
key, SMTP credentials, and Slack webhook URLs as repository secrets or in a secrets manager. Never commit them to the repository.Going further
- Contract Intelligence Pipeline — include milestone due date extraction in your weekly report.
- Chat API reference — citation format, conversation scoping, and the full SSE event schema.
- Add multi-project reporting by looping over a list of project IDs and delivering a combined digest with one conversation per project.