| # | Process | Dept | Frequency | Hrs/Wk | People | Error Rate | Pain Score | Tools |
|---|---|---|---|---|---|---|---|---|
| 1 | Supplier quote collection & parsing | Procurement | Ongoing | 8 | 1 | 8% | 22 | Gmail + Excel |
| 2 | Quote evaluation & comparison | Procurement | ~10/week | 5 | 2 | — | 19 | Excel |
| 3 | Demand intake & approval | Ops / Procurement | ~15/week | 4 | 2 | — | 17 | Email + Excel |
| 4 | Price negotiation | Procurement | ~5/week | 6 | 1 | — | 16 | |
| 5 | RFQ creation & distribution | Procurement | ~10/week | 6 | 1 | — | 14 | Word + Email |
| 6 | Final approval & PO creation | Procurement / Finance | ~10/week | 3 | 2 | — | 13 | Email + SAP |
| TOTAL | 32 | ~1.5 FTE | ~49,920 EUR/yr | |||||
- 1. Quote parsing — multi-language emails (HU/EN/DE), inconsistent formats, 8% error rate, 8 hrs/week
- 2. No audit trail — approvals happen in email threads, no traceable chain for auditors, compliance risk
- 3. Unstructured negotiation — no price guardrails, no concession tracking, manual language switching
- 4. No historical data — supplier performance not tracked, same evaluation mistakes repeated
- 5. Cascading delays — missed approval emails delay entire procurement chain by days, causing stockouts
Demand intake & approval
Manual email chains for approval. No audit trail. Approvals lost in inbox.
RFQ creation & distribution
Copy-paste from demand into Word template. Sent individually per supplier. Must translate to HU/EN/DE.
Supplier quote collection & parsing
Biggest pain. Manually copying prices from emails/PDFs into comparison spreadsheet. 8% error rate.
Quote evaluation & comparison
Manual scoring in spreadsheet. No historical supplier data. Subjective decisions.
Price negotiation
Time-consuming back-and-forth. No negotiation history. Must write in supplier language (HU/EN/DE).
Final approval & PO creation
Another approval round via email. Manual SAP entry for PO. Delays cascade to stockouts.
Gmail
PostgreSQL 16
LiteLLM + Claude
SAP Business One
| State | Timeout | Action |
|---|---|---|
| PENDING_APPROVAL | 168h (7 days) | Token expires, admin notified |
| COLLECTING_RESPONSES | 72h (3 days) | Proceed with available quotes |
| NEGOTIATING | 72h per round | Proceed with last offer |
| PENDING_FINAL_APPROVAL | 168h (7 days) | Token expires, admin notified |
| Phase | Period | Description |
|---|---|---|
| Shadow mode | Week 1-2 | Automation runs, human verifies 100% of outputs |
| Assisted mode | Week 3-4 | Auto-proceed for confidence ≥ 0.95, human reviews remainder |
| Autonomous mode | Month 2+ | Full automation with 5-10% weekly audit sample |
Receive webhook
POST /webhook/demand-intake — accept JSON with demand details
Validate required fields
item_name, quantity, unit, urgency, department, requester_name, requester_email, supplier_category
Generate demand ID
Sequential: DEM-YYYY-NNNN (e.g., DEM-2026-0001)
Insert demand + approval token
Create record with status=DEMAND_RECEIVED, generate 64-char hex token
Audit log: demand_received
Log creation event with full demand details
Render approval email
Jinja2 template with item details, estimated cost, approve/reject buttons
Send approval email
Subject: "[Approval Required] DEM-2026-0001 — A4 Copy Paper"
Update status: PENDING_APPROVAL
Demand now awaits human click on approve/reject link
Return HTTP 200
Response: { status: "ok", demand_id: "DEM-2026-0001", next_status: "PENDING_APPROVAL" }
Human clicks approve/reject link
GET /webhook/approval?token=abc...&action=approve|reject
Validate token format
Must be 64-char hex, action must be approve or reject
Lookup token in database
Match against approval_token OR final_approval_token
Validate: found, not expired, correct status
Token expiry: 168h. Detect initial vs. final approval type.
Apply decision
Update status: APPROVED / REJECTED / FINAL_APPROVED. Clear used token.
Audit log: approval_decision
Actor: human, action type, approval type (initial/final)
Return HTML confirmation
Friendly confirmation page with demand summary
Trigger downstream
Initial approve → POST /webhook/rfq-send. Final approve → PO_CREATED → COMPLETED.
Receive trigger from W2
POST /webhook/rfq-send with demand_id
Load demand details
Python subprocess: load_demand.py
Query matching suppliers
Filter by supplier_category, active=true
Set RFQ deadline (NOW + 72h)
Update status: RFQ_SENT
Render RFQ emails per supplier
Jinja2 templates: hu.html, en.html, de.html — subject includes [RFQ-{demand_id}]
Send RFQ emails (loop over suppliers)
SplitInBatches → Gmail Send per supplier
Log each RFQ sent
Audit: supplier name, email, language, deadline
Gmail trigger — poll every 60s
Filter: subject contains "[RFQ-"
Extract demand ID from subject
Regex: /\[RFQ-(DEM-\d{4}-\d{4})\]/i
Identify supplier
Match sender email to suppliers table (demo: extract [supplier_id:N] footer)
Parse email with AGT-P01
AI extracts: response_type, items[], prices, delivery, terms, confidence
Route by confidence
≥0.90: auto-accept | 0.70-0.89: flag review | <0.70: dead letter
Store parsed quote
Insert into quotes table with round number, dedup by Gmail Message-ID
Check collection completeness
All suppliers responded OR deadline passed?
If complete: trigger W4a Evaluate
Set status EVALUATING, POST /webhook/evaluate
| Confidence | Status | Action |
|---|---|---|
| ≥ 0.90 | success | Auto-accepted, stored in quotes table |
| 0.70 - 0.89 | low_confidence | Stored but flagged: human_verified=false |
| < 0.70 | error | Dead letter queue + admin notification |
| Dimension | Weight | Formula |
|---|---|---|
| Price | 40% | MAX(0, 1 - (unit_price - min_price) / min_price) × 40 |
| Delivery | 20% | MAX(0, 1 - delivery_days / max_delivery) × 20 |
| Quality | 25% | historical_quality_score × 25 |
| Reliability | 15% | historical_ontime_rate × 15 |
| Condition | Action |
|---|---|
| Any supplier price > max_acceptable_price | Trigger W4b: negotiate with those suppliers |
| All prices ≤ max_acceptable_price | Skip negotiation → send final approval email |
| # | Rule | On Violation |
|---|---|---|
| 1 | proposed_price ≤ max_acceptable_price | Block send |
| 2 | Quantity unchanged | Block send |
| 3 | Payment terms ≥ 14 days | Block send |
| 4 | Management disclaimer included | Auto-append |
| 5 | Language matches supplier preference | Flag for review |
| Round | Action | Rationale |
|---|---|---|
| Round 1-2 | Auto-send via Gmail | Standard counter-proposals, low risk |
| Round 3+ | Hold as draft, notify admin | Requires human judgment to prevent over-negotiation |
Evaluate: should_negotiate?
AGT-P02 checks if any price exceeds max_acceptable_price
Draft & send counter-proposals
AGT-P03 drafts, guardrails validate, send to flagged suppliers
Monitor supplier replies (round N)
Same W3b flow, stores quotes with round=N
Re-evaluate with round N quotes
Negotiate again or proceed to final approval?
Supplier Email Parser
Quote Evaluator
Negotiation Drafter
| Layer | Component | Handles |
|---|---|---|
| Layer 1 | LiteLLM Proxy | Rate limits (2x retry), timeouts (60s), provider fallback |
| Layer 2 | agent_runner.py | Malformed JSON (retry with fix), schema validation, confidence routing |
| Layer 3 | n8n Workflows | Per-step error branches, dead letter queue, admin email alerts |
n8n Code node calls run_agent.py
Python subprocess with JSON on stdin: { agent_id, input_data, context }
agent_runner loads config from DB
Fetch agent_configs: model, temperature, prompt path, output schema, thresholds
Resolve prompt + append shared rules
Load prompt file + anti-hallucination.md + output-format.md
POST to LiteLLM /v1/chat/completions
OpenAI-compatible API on port 4000, routes to Claude Sonnet 4
Validate response against schema
JSON parse, schema validation, confidence check, status assignment
Return AgentResult to n8n
{ status, data, confidence, reasoning, agent_id, model_used }