A production-grade, fully dynamic multi-stage approval workflow engine built on Payload CMS v3, Next.js 15, TypeScript, and MongoDB.
Built as a backend engineering challenge for WeFrameTech β designed and implemented from scratch with zero prior Payload CMS experience.
- What It Does
- Architecture and Diagram
- Setup Instructions
- Demo Credentials
- Sample Workflows
- API Endpoints
- Deployment Guide
- Tech Stack
NovaCorp's internal teams can create, assign, and track multi-stage approval workflows for any document type (Contracts, Blogs, Product Specs, etc.) directly from the Admin UI β no code changes required.
Before this system, approvals were handled manually via email chains with no visibility, no audit trail, and no enforcement of who should approve what. Now:
- Every document has a defined approval path
- The right person is automatically notified and assigned
- Every action is permanently logged and immutable
- Steps are conditionally skipped based on document values
- The full workflow history is visible inline inside the document
Sarah (editor) submits a contract for βΉ75,000
β
Step 1 β Legal Review
Auto-assigned to Priya (legal)
Priya sees Approve / Reject / Comment buttons
β Priya approves
Step 2 β Manager Approval [condition: amount > 10,000 β
]
Auto-assigned to Arjun (manager)
β Arjun approves
Step 3 β Director Sign-off
Auto-assigned to Raj (director)
β Raj signs off
Contract status β Executed β
Audit trail preserved forever
For a small contract (βΉ5,000), Step 2 is automatically skipped β the system evaluates amount > 10000 and proceeds directly to Director Sign-off.
All workflow logic lives in a custom Workflow Engine that runs server-side, triggered by Payload's lifecycle hooks. The architecture is best understood through three core flows
src/
βββ collections/
β βββ Users.ts β Auth system + role-based access control
β βββ Workflows.ts β Dynamic workflow definitions (unlimited steps)
β βββ WorkflowLogs.ts β Immutable audit trail (update/delete blocked)
β βββ Contracts.ts β Legal agreements with workflow support
β βββ Blogs.ts β Internal articles with workflow support
β βββ Media.ts β File upload handling
β
βββ plugins/
β βββ WorkflowEngine.ts β Core plugin (the brain of the system)
β
βββ components/
β βββ WorkflowPanel.tsx β React UI injected into every document edit view
β
βββ payload.config.ts β Root config β wires everything together
The WorkflowEngine is a Payload plugin that dynamically injects afterChange hooks into any collection you tell it to watch:
workflowEnginePlugin({
watchedCollections: ['contracts', 'blogs', 'product-specs']
})When a document is saved, the plugin:
1. Checks if a workflow is attached to the document
2. Iterates through workflow steps
3. Evaluates each step's condition against the document data
4. Skips steps whose conditions fail
5. Finds the first user matching the step's assigned role
6. Creates an immutable WorkflowLog entry
7. Updates the document status automatically
8. Fires a console notification (simulated email)
Steps support simple field-based expressions:
amount > 10000 β runs only for high-value contracts
amount <= 5000 β runs only for small contracts
status == draft β runs only if document is still draft
estimatedCost >= 50000 β runs for expensive product specs
The evaluator parses the expression, resolves the field value from the document, and returns true/false. Steps with failing conditions are automatically skipped and logged.
A React component ('use client') injected via Payload's ui field type into every document's edit view. It:
- Fetches live workflow status via
/api/workflow-status - Shows current step, assigned user, and status badge
- Renders Approve / Reject / Comment buttons only for the assigned user
- Shows an observer message to all other users
- Displays the full chronological audit trail with color-coded actions
- Dynamically detects which collection it's in from the URL
Admin UI WorkflowEngine Plugin MongoDB
βββββββββ βββββββββββββββββββββ βββββββ
Create Contract β afterChange hook fires
Attach Workflow β Find workflow by ID β workflows collection
Save Document β Evaluate step conditions
β Find user by role β users collection
β Create WorkflowLog β workflow-logs collection
β Update document status β contracts collection
β Console notify assigned user
Assigned User opens doc β WorkflowPanel fetches status β workflow-logs collection
Clicks Approve β POST /api/workflow-action
β Log action β workflow-logs collection
β Evaluate next step
β Assign next user β users collection
β Update status β contracts collection
- Node.js 18 or higher
- MongoDB β local instance or MongoDB Atlas (free tier works)
- npm or yarn
git clone https://github.com/alamrumman/workflow-system.git
cd workflow-systemnpm installThis may take 2-3 minutes on first install due to Payload CMS dependencies.
Create a .env file in the project root:
# For local MongoDB
DATABASE_URL=mongodb://127.0.0.1/workflow-system
# For MongoDB Atlas
# DATABASE_URL=mongodb+srv://<username>:<password>@cluster.mongodb.net/workflow-system
# Any random string β used to sign auth tokens
PAYLOAD_SECRET=your-secret-key-change-this-in-productionnpm run devWait for the terminal to show:
β Ready in Xs
Open http://localhost:3000/admin in your browser. You'll see a "Create First User" screen. Create your admin account.
npm run seedThis automatically creates all 5 demo users and 2 sample workflows. See Demo Credentials below.
- Go to Workflow Engine β Workflows β Create New
- Set name, applies-to collection, and add steps
- Go to Business Documents β Contracts β Create New
- Fill in the fields and select your workflow under "Attached Workflow"
- Save β the workflow fires automatically
- Check the Workflow Status panel at the bottom of the contract
| Name | Password | Role | What They Do | |
|---|---|---|---|---|
| Admin | alamrumman0@gmail.com | 12345 | admin | Manages system, creates workflows |
| Sarah | sarah@novacorp.com | sarah12345 | editor | Creates contracts and blog posts |
| Priya | priya@novacorp.com | priya12345 | legal | Reviews contracts for legal risk |
| Arjun | arjun@novacorp.com | arjun12345 | manager | Approves high-value contracts |
| Raj | raj@novacorp.com | raj12345 | director | Final sign-off on all contracts |
Applies to: contracts
| Step | Name | Assigned Role | Condition | Step Type |
|---|---|---|---|---|
| 1 | Legal Review | legal | (always runs) | review |
| 2 | Manager Approval | manager | amount > 10000 |
approval |
| 3 | Director Sign-off | director | (always runs) | sign-off |
Scenario A β Small Contract (βΉ5,000):
Legal Review (Priya) β approved
Manager Approval β SKIPPED (5000 is not > 10000)
Director Sign-off (Raj) β approved
Status: Executed β
Scenario B β Large Contract (βΉ75,000):
Legal Review (Priya) β approved
Manager Approval (Arjun) β approved (75000 > 10000 β
)
Director Sign-off (Raj) β approved
Status: Executed β
Scenario C β Rejected Contract:
Legal Review (Priya) β rejected with comment: "Missing termination clause"
Status: Rejected β
Full audit trail preserved
Applies to: blogs
| Step | Name | Assigned Role | Condition | Step Type |
|---|---|---|---|---|
| 1 | Editorial Review | editor | (always runs) | review |
| 2 | Manager Approval | manager | (always runs) | approval |
Blog submitted
Editorial Review (editor) β approved
Manager Approval (Arjun) β approved
Status: Published β
Manually trigger a workflow on any document.
Request Body:
{
"workflowId": "64abc123...",
"docId": "64def456...",
"collection": "contracts"
}Success Response:
{
"success": true,
"message": "Workflow triggered for contracts #64def456...",
"workflow": "Contract Approval Flow",
"firstStep": "Legal Review"
}Error Responses:
{ "error": "Missing workflowId, docId, or collection" } // 400
{ "error": "Document not found" } // 404
{ "error": "Workflow not found" } // 404Returns the current workflow state and complete audit trail for any document.
Example Request:
GET /api/workflow-status?docId=64def456abc123
Success Response:
{
"docId": "64def456abc123",
"currentStep": "Manager Approval",
"currentAction": "triggered",
"totalLogs": 3,
"logs": [
{
"stepName": "Manager Approval",
"action": "triggered",
"userEmail": "arjun@novacorp.com",
"comment": null,
"timestamp": "2026-03-07T12:30:00.000Z"
},
{
"stepName": "Legal Review",
"action": "approved",
"userEmail": "priya@novacorp.com",
"comment": "All clauses verified",
"timestamp": "2026-03-07T11:45:00.000Z"
},
{
"stepName": "Legal Review",
"action": "triggered",
"userEmail": "priya@novacorp.com",
"comment": null,
"timestamp": "2026-03-07T10:00:00.000Z"
}
]
}Payload CMS v3 is built on Next.js, making Vercel the simplest deployment target.
- Go to mongodb.com/atlas
- Create a free M0 cluster
- Create a database user: Database Access β Add New Database User
- Allow all IPs: Network Access β Add IP Address β 0.0.0.0/0
- Get your connection string: Connect β Drivers β Copy connection string
It looks like:
mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/workflow-system
Make sure your repository is private (required by the task):
git add .
git commit -m "NovaCorp Workflow Management System"
git push origin main- Go to vercel.com and sign in
- Click Add New β Project
- Import your GitHub repository
- Keep the default build settings (Vercel auto-detects Next.js)
In the Vercel project settings before deploying:
| Variable | Value |
|---|---|
DATABASE_URL |
Your MongoDB Atlas connection string |
PAYLOAD_SECRET |
Any strong random string (min 32 chars) |
Click Deploy. Vercel will build and deploy automatically. First build takes 3-5 minutes.
Visit https://your-project.vercel.app/admin and create your first admin user.
Either create workflows and users manually in the admin panel, or update your local .env to point to Atlas and run:
npm run seedThis seeds your Atlas database directly.
- Go to render.com β New Web Service
- Connect your GitHub repository
- Set build command:
npm install && npm run build - Set start command:
npm start - Add environment variables (
DATABASE_URL,PAYLOAD_SECRET) - Deploy
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
β | MongoDB connection string |
PAYLOAD_SECRET |
β | Secret key for auth token signing (min 32 chars) |
| Technology | Version | Purpose |
|---|---|---|
| Payload CMS | v3.x | Headless CMS, collections, admin UI |
| Next.js | 15.x | React framework (Payload v3 runtime) |
| TypeScript | 5.x | Type safety throughout |
| MongoDB | 7.x | Database |
| Mongoose | 8.x | MongoDB adapter for Payload |
| React | 19.x | Admin UI components |
Why a plugin instead of direct hooks? The plugin pattern makes the workflow engine completely reusable β adding a new collection to the workflow system requires only one line of config change. No duplicated hook logic across collections.
Why UI field for the WorkflowPanel?
Payload v3 removed afterFields from admin.components. The ui field type is the correct v3 approach β it renders a custom React component inline within the collection edit form, giving full access to Payload's React context including useDocumentInfo and useAuth.
Why query params instead of URL params for the status endpoint?
Payload v3's custom endpoint routing has issues with dynamic URL segments (:docId). Query parameters (?docId=xxx) are more reliable and work consistently across all environments.
Why is WorkflowLogs immutable?
The audit trail must be trustworthy. By setting update: () => false and delete: () => false on the access control, the logs become a permanent, tamper-proof record of all workflow actions regardless of user role.
Built by Rumman Alam
- GitHub: @alamrumman
- Stack: Node.js Β· Express Β· React Β· MongoDB Β· TypeScript
This project was built in under 24 hours with zero prior Payload CMS experience as part of a backend engineering hiring challenge.