RequestOps

A personal project automating the college event-permission workflow: a student submits a request through a live web form, it moves through a two-stage approval chain — Club Lead, then Department Head — entirely inside Notion, and on final approval the system generates a real PDF permission slip and emails it automatically. Every step along the way writes a code-attributed audit log.

Live: https://requestops.onrender.com


Architecture

Trigger (styled web form, deployed)
        │
        ▼
  Backend Service (Node.js / Express, hosted on Render)
        │
        ├─▶ (if structured fields are left blank) AI extraction fills them in from free text
        │
        ▼
  Notion "Requests" DB  ──▶  Page created, Status = Pending, Approval Stage = Club Lead
        │
        ▼
  Club Lead reviews & decides inside Notion
        │
        ├── Rejected ──▶ Rejection email sent, Run Log written, request closed
        │
        └── Approved ──▶ "Awaiting department head" email sent
                          Approval Stage flips to Department Head, Status resets to Pending
                                  │
                                  ▼
                          Department Head reviews & decides inside Notion
                                  │
                                  ├── Rejected ──▶ Rejection email sent, Run Log written, request closed
                                  │
                                  └── Approved ──▶ PDF permission slip generated
                                                    Emailed with the PDF attached
                                                    Run Log written, request closed

A cron job on the deployed server checks Notion every 2 minutes for decisions at either stage — fully autonomous, no manual triggers required at any point.

Notion is the only interface either approver ever touches — no separate dashboard, no code, no terminal.


Status: Complete

  • ✅ Live, deployed intake form — styled, hosted at the URL above
  • ✅ AI extraction (Groq) — fills in event name/date/venue from free text only when those fields are left blank; zero AI calls when the form is filled in cleanly
  • ✅ Two-stage approval — Club Lead approval moves a request to Department Head review; a rejection at either stage ends the request immediately without bothering the next approver
  • ✅ PDF permission slip — generated in-memory (no disk writes) on final approval, attached directly to the confirmation email
  • ✅ Real email delivery via Resend's HTTP API — switched from Gmail SMTP after discovering Render's free tier blocks outbound SMTP ports (25/465/587) as an anti-abuse measure
  • ✅ Run Log — every row written by code with a real timestamp, never typed by hand
  • ✅ Idempotency — a request is only ever processed once per stage; a failed action retries automatically instead of silently dropping
  • ✅ Fully autonomous — a cron job on the deployed server checks Notion every 2 minutes with zero manual triggers
  • ✅ Status lookup page (/status.html) — students can check their request's current state by reference ID, linked directly from the post-submit confirmation screen
  • ✅ Deployed on Render, verified working end-to-end including both approval stages and PDF delivery, with the server running independently of any local machine

Setup

1. Install

npm install

2. Notion setup

Create an integration at notion.so/my-integrations and copy its access token. Create two full-page databases and connect the integration to both (··· menu → Connections).

"Requests" database

Property Type
Request ID Title
Student Name Text
Email Email
Event Name Text
Event Date Date
Venue Text
Description Text
Status Select — Pending, Approved, Rejected
Approval Stage Select — Club Lead, Department Head
Decision Reason Text — coordinator-owned, code only reads this, never overwrites it
Processed At Date — set by code once the real action has fired for the current stage

Important: property names must match exactly, including no leading/trailing spaces — Notion's API treats " Approval Stage" and "Approval Stage" as two entirely different properties, and this cost real debugging time once already.

"Run Log" database

Property Type
Run ID Title
Request ID Text
Status Select — Created, Success, Failed
Timestamp Date (with time)
Outcome Text
Error Text

3. Environment variables

Copy .env.example to .env and fill in real values — Notion token/database IDs, a Resend API key, and a Groq API key.

4. Run locally

node src/server.js

Visit http://localhost:3000.

5. Deploy

Push to GitHub, connect the repo on Render as a Web Service (build command npm install, start command node src/server.js), and add every variable from .env to Render's Environment tab (except PORT, which Render sets automatically).


How each piece works

  • src/notion.js — all Notion API calls: creating requests, querying for stage-specific decisions (getStage1Decisions, getStage2Decisions), advancing a request from Club Lead to Department Head, writing Run Log rows, and locking a request as processed.
  • src/email.js — sends every email variant (stage-1 approval notice, final approval with PDF attached, rejection) via Resend's HTTP API.
  • src/pdf.js — generates the permission slip PDF entirely in memory using PDFKit, returning a Buffer that gets attached directly to the final approval email. Nothing is ever written to disk, since Render's free tier doesn't guarantee persistent storage between requests.
  • src/ai.js — the load-bearing AI step. looksMessy() decides deterministically whether AI is even needed; extractFieldsFromText() calls Groq's API and only fires when structured form fields were left blank.
  • src/server.js — the public form route, the status lookup route, the two-stage decision processor, and the cron schedule that runs it automatically.
  • public/index.html — the student-facing form, with a confirmation state that links directly through to the status page for the request just created.
  • public/status.html — lets a student check their request's current state by reference ID, including the coordinator's rejection reason when applicable.

Design principles this project follows

  • Notion is the only approver interface, at both stages. No separate dashboard was built for either the Club Lead or the Department Head — approving is changing one field, nothing more.
  • A rejection at any stage ends the request immediately. The system never bothers a later approver with something already rejected earlier — getStage2Decisions() simply never sees a request that was rejected at Stage 1, because it only queries for Approval Stage: Department Head, which a Stage-1-rejected request never reaches.
  • AI is narrow and conditional, never decorative. It only runs when structured fields are genuinely missing — never as a wrapper around something a simple if statement could already do.
  • The coordinator's own input is never overwritten. Decision Reason is read by code, never written to, so a rejection reason typed by either approver survives all the way into the email the student receives.
  • Failures don't fail silently. If an email send or PDF generation fails partway, the request is not marked as processed for that stage — it retries automatically on the next cron cycle.

Known considerations (not blockers, just honest notes)

  • Render's free tier spins the server down after inactivity, which also stops the cron scheduler entirely while asleep — a decision made in Notion won't be acted on until something wakes the server back up.
  • Resend's free tier, without a verified custom domain, only delivers to the email address the Resend account itself is signed up with — not to arbitrary addresses typed into the form. Fine for personal testing; would need domain verification to email real, distinct student inboxes.
  • Running the server both locally and on Render at the same time can cause the same decision to be processed twice (two independent cron loops racing to claim the same unprocessed row) — occasionally results in a duplicate email. Low-stakes for a personal project; run only one instance at a time to avoid it.
  • No automated test suite — every piece, including both approval stages and the PDF attachment, was manually verified end-to-end during development. Future changes should be re-tested manually the same way.

Built With

Share this project:

Updates