Contents
- Invoice Approval Pipeline — pg_durable Demo
- What This Shows
- Scenario (Plain English)
- Pipeline Flowchart
- Request / Response Shape
- Directory Layout
- Prerequisites
- Setup
- 10-Minute Demo Script
- Minute 0–1: The Problem
- Minute 1–2:30: Show the SQL
- Minute 2:30–3:30: Visualize the Graph
- Minute 3:30–4:30: Start the Pipeline
- Minute 4:30–5:30: Watch It Work
- Minute 5:30–6:00: Show the Live Graph
- Minute 6:00–6:30: Human Approves
- Minute 6:30–7:00: Confirm Approval
- Minute 7:00–8:00: Feed More Invoices
- Minute 8:00–9:00: Show the Pipeline Keeps Going
- Minute 9:00–9:30: Final State
- Minute 9:30–10:00: Wrap Up
- Cleanup
- Operational Notes
Invoice Approval Pipeline — pg_durable Demo
An always-on invoice processing pipeline that classifies invoices via an Azure Function, auto-approves small ones, and pauses for human approval on high-value invoices — all orchestrated from SQL inside PostgreSQL.
What This Shows
| pg_durable Feature | How It Appears |
|---|---|
Infinite Loop (@>) |
Pipeline polls for new invoices continuously |
HTTP / Azure Functions (df.http()) |
Calls a deployed Azure Function to classify each invoice |
Human-in-the-Loop (df.wait_for_signal) |
High-value invoices (> $10K) pause until a human approves |
Conditional Branching (df.if) |
Routes invoices through auto-approve or approval-required paths |
Named Results (|=>) |
Passes data between steps (invoice → request body → HTTP response → decision) |
Visualization (df.explain) |
Shows both the static graph and live execution status |
Monitoring (df.list_instances, df.status) |
Observe the pipeline in real time |
Scenario (Plain English)
Invoices arrive in a PostgreSQL table. A background pipeline picks each one up, sends it to an Azure Function that reads the amount and categorizes it (supplies, consulting, hardware, etc.). Small invoices are approved automatically. Large invoices (over $10,000) are flagged and the pipeline waits for a human to approve or reject them. New invoices can arrive at any time — the loop picks them up on its next pass.
Pipeline Flowchart
flowchart TD
START((Start)) --> LOOP
subgraph LOOP ["♻️ Infinite Loop"]
POLL["Fetch one pending invoice"]
POLL --> HAS_WORK{Found one?}
HAS_WORK -- No --> WAIT_POLL["Sleep 5s"]
WAIT_POLL --> POLL
HAS_WORK -- Yes --> MARK["Mark 'processing'"]
MARK --> HTTP["☁️ Call Azure Function<br/><i>classify_invoice</i>"]
HTTP --> PARSE["Parse HTTP response"]
PARSE --> OK{Classification<br/>succeeded?}
OK -- No --> FAIL["Mark 'failed'<br/>+ audit log"]
FAIL --> PAUSE
OK -- Yes --> UPDATE["Update vendor,<br/>category, amount"]
UPDATE --> THRESHOLD{Amount<br/>> $10,000?}
THRESHOLD -- No --> AUTO["✅ Auto-approve<br/>+ audit log"]
AUTO --> PAUSE
THRESHOLD -- Yes --> FLAG["Flag 'awaiting_approval'<br/>+ audit log"]
FLAG --> SIGNAL["⏳ Wait for signal<br/><i>'approval' (5 min timeout)</i>"]
SIGNAL --> APPROVED{Approved?}
APPROVED -- Yes --> APPROVE["✅ Mark 'approved'<br/>+ audit log"]
APPROVED -- No / Timeout --> REJECT["❌ Mark 'rejected'<br/>+ audit log"]
APPROVE --> PAUSE
REJECT --> PAUSE
PAUSE["Sleep 2s"] --> POLL
end
Request / Response Shape
Request (sent to Azure Function):
json
{
"invoice_id": 2,
"description": "GlobalTech Consulting - Cloud infrastructure advisory",
"raw_amount": "$24,500.00"
}
Response (from Azure Function):
json
{
"invoice_id": 2,
"vendor": "GlobalTech Consulting",
"category": "consulting",
"amount": 24500.00,
"currency": "USD",
"requires_approval": true,
"confidence": 0.92
}
Directory Layout
examples/invoice-approval/
├── README.md ← you are here
├── function-app/
│ ├── host.json
│ ├── requirements.txt
│ └── classify_invoice/
│ ├── __init__.py ← deterministic classifier (no AI dependency)
│ └── function.json
├── scripts/
│ ├── create_function_app.sh
│ ├── deploy_function.sh
│ ├── configure_pg.sh
│ ├── cleanup_azure.sh
│ ├── feed_invoices.sh ← insert random invoices mid-demo
│ ├── smoke_check.sh ← offline syntax/config validation
│ └── live_smoke_check.sh ← deployed Azure Function check
└── sql/
├── 01_schema.sql ← tables + truncate
├── 02_set_vars.sql ← df.setvar for URL/key
├── 03_seed_data.sql ← 2 invoices (1 small, 1 large)
├── 04_explain.sql ← dry-run: preview the graph
├── 05_start_workflow.sql ← launch the pipeline
├── 06_monitor.sql ← check invoice status + audit trail
├── 07_approve.sql ← send approval signal
├── 08_explain_live.sql ← live graph with ✓/⏳ markers
├── 09_verify.sql ← final state summary
└── 10_cancel.sql ← stop the pipeline
Prerequisites
- Azure CLI (
az) installed and logged in (az login) - Azure Functions Core Tools (
func) - PostgreSQL with pg_durable enabled
psqlavailable (system or pgrx)
Setup
1) Provision Azure Function App
cd examples/invoice-approval
chmod +x scripts/*.sh
./scripts/create_function_app.sh -l eastus
2) Deploy the classifier function
./scripts/deploy_function.sh
3) Smoke-check the function
./scripts/live_smoke_check.sh
4) Create demo schema
psql -d postgres -p 28817 -f sql/01_schema.sql
5) Configure pg_durable variables
./scripts/configure_pg.sh -d postgres -p 28817
6) Insert seed data
psql -d postgres -p 28817 -f sql/03_seed_data.sql
10-Minute Demo Script
Minute 0–1: The Problem
“Invoices come in. Small ones can be auto-approved, but anything over $10K needs a human to sign off. We want this to run continuously inside PostgreSQL — no external job queue, no microservices.”
Minute 1–2:30: Show the SQL
Open sql/05_start_workflow.sql and walk through the structure:
- The infinite loop (@>)
- The Azure Function call (df.http)
- The branching (df.if on amount threshold)
- The signal wait (df.wait_for_signal)
Minute 2:30–3:30: Visualize the Graph
psql -d postgres -p 28817 -f sql/04_explain.sql
This shows the df.explain() dry-run — the tree structure of the pipeline without executing it. Also show the Mermaid diagram above.
Minute 3:30–4:30: Start the Pipeline
psql -d postgres -p 28817 -f sql/05_start_workflow.sql
Note the instance ID returned. The pipeline immediately starts processing the 2 seeded invoices.
Minute 4:30–5:30: Watch It Work
psql -d postgres -p 28817 -f sql/06_monitor.sql
You should see: - Invoice #1 ($3,420 — office supplies): auto-approved ✅ - Invoice #2 ($24,500 — consulting): awaiting_approval ⏳
Minute 5:30–6:00: Show the Live Graph
SELECT df.explain('<instance-id>');
The signal wait node shows ⏳.
Minute 6:00–6:30: Human Approves
SELECT df.signal('<instance-id>', 'approval', '{"approved": true, "approver": "demo-user"}');
Minute 6:30–7:00: Confirm Approval
psql -d postgres -p 28817 -f sql/06_monitor.sql
Invoice #2 is now approved, audit trail shows the approver.
Minute 7:00–8:00: Feed More Invoices
In another terminal:
./scripts/feed_invoices.sh -d postgres -p 28817 -n 3
Wait a few seconds, then monitor again — the loop picks them up automatically.
Minute 8:00–9:00: Show the Pipeline Keeps Going
psql -d postgres -p 28817 -f sql/06_monitor.sql
New invoices are being processed. Any over $10K will pause for signals.
Minute 9:00–9:30: Final State
psql -d postgres -p 28817 -f sql/09_verify.sql
Minute 9:30–10:00: Wrap Up
“This entire pipeline — HTTP calls, human approval gates, infinite loops, conditional logic — runs inside PostgreSQL. No external orchestrator. Survives crashes. All visible through SQL.”
Optionally cancel the pipeline:
SELECT df.cancel('<instance-id>', 'Demo complete');
Cleanup
./scripts/cleanup_azure.sh -y
Operational Notes
- The classifier Azure Function is deterministic (keyword-based, no AI). It always returns consistent results for the same input.
- The $10,000 threshold is hardcoded in the SQL workflow — change it in
05_start_workflow.sqlto adjust. - The approval signal has a 5-minute timeout. If no signal is sent, the invoice is automatically rejected.
feed_invoices.sh -s 10runs continuously, inserting a batch every 10 seconds.