---
title: "AP invoice routing and approval"
excerpt: "Watches an inbox folder for invoices, extracts vendor and amount with Claude, routes to the right approver via Slack based on amount thresholds, and tracks every approval in Dock."
category: "Template"
---

# Agent: read this first

    You're working in the AP invoice routing and approval workspace at trydock.ai/<org>/ap-invoice-routing-approval.

    ## Your role

    Watches invoices-inbox/ on cron, extracts invoice fields with Claude, creates Invoice queue rows at the right Status (Approved for auto, Pending otherwise), routes Slack approval requests, never flips a Pending row to Approved.

    ## Cadence, when to update Dock

    - After EVERY tool call that changes state (row create / update, doc append), append an entry to the **Status** surface.
    - After every milestone, update the canonical doc surface for that work.
    - When you start a session, READ the last 5 Status entries first to know what happened since you last worked.

    ## User-loop protocol

    - End EVERY reply to the user with: "Check Dock for the latest at trydock.ai/<org>/ap-invoice-routing-approval."
    - If you make a decision the user might want to override (priority, scope cut, tone change), append to **Status** AND mention it in your reply: "Decided X. If you'd rather Y, edit [surface] and I'll re-plan."
    - If you're blocked, post to Status with prefix `BLOCKED:` and ask one specific question.

    ## Don't touch

    - Canonical phase / column / surface titles.
    - Anything in a section titled "Locked" or "Decisions sealed."
    - The agentPrompt itself (this section).

    ## First tool calls

    1. `list_surfaces(workspace_slug="ap-invoice-routing-approval")`
    2. `get_doc(workspace_slug="ap-invoice-routing-approval", surface_slug="status")`
    3. `list_rows(workspace_slug="ap-invoice-routing-approval", surface_slug="invoice-queue")`

    ---

    # AP invoice routing and approval

    A two-step pipeline that turns an inbox folder of invoices into a routed approval queue. Open in Dock and you get four surfaces seeded:

    - **Invoice queue** (table) one row per invoice: Vendor, Invoice Number, Amount, Due Date, Department, GL Code, Status, Invoice File, Notes, Submitted At, Approver, Approved At, Approved By.
    - **Setup guide** (doc) how the agent watches the inbox folder + the two Python scripts + threshold + approver configuration.
    - **Approval log** (doc) running log of every routing decision. Useful for audit and for spotting routing patterns over a month.
    - **Status** (doc) the agent's working session log. End of every run: 1 paragraph of what was processed, what was routed, what's pending.

    ## Bring your own agent

    Connect one agent to this workspace (Claude in Cursor, Claude Code, Codex, any MCP client). The agent watches the inbox folder on cron, no manual trigger needed once schedules are wired.

    ## The user-loop protocol

    Your agent proposes; you decide.

    - Hourly (or as you prefer): agent scans invoices-inbox/ for new .txt or .md files. Skips names in processed_invoices.json. For each new file, extracts vendor, invoice number, amount, due date, department, GL code, and a one-sentence notes summary via Claude.
    - If amount below AUTO_APPROVE_UNDER (default 500): create Invoice queue row at Status=Approved, post auto-approved confirmation to Slack.
    - If amount between AUTO_APPROVE_UNDER and HIGH_VALUE_ABOVE (default 5000): create row at Status=Pending, route to APPROVER_STANDARD via Slack approval request.
    - If amount above HIGH_VALUE_ABOVE: create row at Status=Pending, route to APPROVER_HIGH_VALUE via Slack approval request.
    - The approver opens Dock, reviews, flips Status to Approved or Rejected. Daily 9 AM: process_approvals.py sweeps for Status=Approved rows without an Approved At timestamp, marks them Payment Ready, posts to Slack.
    - Agent NEVER flips Pending rows to Approved. Agent NEVER moves money. Those are operator decisions.
    - End of every working session, agent writes 1 paragraph to **Status**: invoices processed, routed, auto-approved, anything that failed extraction.

    ## First run

    1. Confirm thresholds + approvers with the operator. AUTO_APPROVE_UNDER, HIGH_VALUE_ABOVE, APPROVER_STANDARD (Slack user ID), APPROVER_HIGH_VALUE (Slack user ID).
    2. Open Setup guide and follow the install steps. Configure .env, drop scripts.
    3. Drop 1-2 test invoices (plain text) into invoices-inbox/. Run route_invoices.py once manually. Watch Invoice queue + Slack + Status.

    ## Status

    ### [00:00 UTC] seeded by Dock
    Workspace created from the AP invoice routing and approval template. Agent: read the Setup guide, then confirm thresholds + approver Slack IDs with the operator before the first run.
    Next: confirm thresholds + approvers, run first invoice test.

## Outcome

An AP workspace where every invoice gets one row, the right approver gets pinged in Slack with full context, and Payment Ready rows are the single source of truth for what's cleared to pay.

**Estimated time:** 45 min setup, ongoing ~2 min per invoice review  
**Difficulty:** beginner  
**For:** Controllers, office managers, and ops leads handling 20-200 invoices/month with multiple approver tiers.

## What you'll need

Pre-register or install before you start.

- **[Dock](https://trydock.ai)** _(Free plan covers this template; Pro $19/mo unlocks more workspaces.)_ — The workspace itself, the Invoice queue table, the approval log doc surface.
- **[Anthropic API](https://www.anthropic.com/api)** _(Pay per token, ~$0.003 per invoice extraction on Sonnet.)_ — Claude extracts vendor, amount, due date, department, GL code, and a one-line notes summary per invoice.
- **[Slack incoming webhook](https://api.slack.com/messaging/webhooks)** _(Free)_ — Approval request destination. Block Kit message with vendor + amount + due date + department + a mention to the right approver.
- **[CueAPI (optional)](https://cueapi.ai)** _(Free tier covers hourly + daily cadence.)_ — Cloud-scheduled hourly route + daily process so the pipeline keeps running when your laptop is closed.

---

# The template · 5 steps

## Step 1: Set routing thresholds + approver Slack IDs

_Estimated time: 5 min_

The whole pipeline pivots on three numbers and two Slack user IDs. AUTO_APPROVE_UNDER is the threshold below which invoices skip approval entirely (default 500). HIGH_VALUE_ABOVE is the threshold above which invoices route to the high-value approver (default 5000). APPROVER_STANDARD and APPROVER_HIGH_VALUE are Slack user IDs (e.g. U12345678).

### Tasks

- [ ] Decide AUTO_APPROVE_UNDER. Below this number, invoices are auto-approved without a Slack ping. Default 500. Higher if you trust historical vendors more.
- [ ] Decide HIGH_VALUE_ABOVE. Above this, invoices route to the high-value approver. Default 5000.
- [ ] Get APPROVER_STANDARD Slack user ID: in Slack, click the approver's profile, More, Copy Member ID. Format like U12345678.
- [ ] Get APPROVER_HIGH_VALUE Slack user ID the same way. Often the CFO or CEO at smaller companies.

> [!CAUTION]
> **Gotchas**
>
> - Slack user IDs (Uxxxxx) not display names. Display names break when someone changes theirs.
> - If APPROVER_STANDARD is blank, the Slack message still posts but without a mention, so nobody gets notified. Always fill both.

## Step 2: Wire .env + the two Python scripts

_Estimated time: 20 min_

Two scripts: route_invoices.py runs on cron (hourly or whatever fits), process_approvals.py runs daily. Both read .env, both are standalone CueAPI handlers. Install Anthropic, requests, dotenv, drop scripts from Setup guide.

### Tasks

- [ ] Open Setup guide (doc) and copy route_invoices.py + process_approvals.py into a local folder
- [ ] Run pip install anthropic requests python-dotenv
- [ ] Create .env with DOCK_API_KEY, DOCK_WORKSPACE_SLUG, ANTHROPIC_API_KEY, SLACK_WEBHOOK_URL, INVOICES_INBOX_DIR=./invoices-inbox, AUTO_APPROVE_UNDER=500, HIGH_VALUE_ABOVE=5000, APPROVER_STANDARD, APPROVER_HIGH_VALUE, CURRENCY_SYMBOL=$, CLAUDE_MODEL=claude-sonnet-4-6
- [ ] Generate a Dock API key at trydock.ai/settings/api
- [ ] Create a Slack incoming webhook for your finance or ops channel, paste the URL into SLACK_WEBHOOK_URL
- [ ] mkdir invoices-inbox; drop a test invoice as a .txt file (plain text vendor/amount/due date)
- [ ] Run python route_invoices.py once manually. Confirm an Invoice queue row appears + a Slack approval request lands in the channel.

> [!CAUTION]
> **Gotchas**
>
> - PDF invoices need pdfplumber. The base template handles .txt and .md only. See the Extending section in Setup guide for the PDF swap.
> - Empty file in inbox: the script skips silently and marks processed. Check the log if you expected a row and didn't get one.

### Agent prompt for this step

```text
Run a first route of the inbox. List new files in INVOICES_INBOX_DIR. For each, run Claude extract. Route based on amount thresholds. Create an Invoice queue row at the right Status. Post the matching Slack message. Append filename to processed_invoices.json on success. Post a Status entry summarizing: processed, routed standard, routed high-value, auto-approved, failed extraction.
```

## Step 3: Test the full approval loop

_Estimated time: 15 min_

Before scheduling, confirm the human-loop works end to end. Drop three test invoices at different amounts (e.g. 100, 1500, 8000). Watch Slack, watch Invoice queue, flip a Pending row to Approved, run process_approvals.py, confirm Payment Ready and the second Slack ping.

### Tasks

- [ ] Drop three test invoices in invoices-inbox/ at amounts that hit each tier: ~100 (auto-approve), ~1500 (standard), ~8000 (high-value)
- [ ] Run python route_invoices.py. Watch Slack: one auto-approved confirmation, two approval requests with the right mentions.
- [ ] In Dock Invoice queue, flip one Pending row's Status to Approved
- [ ] Run python process_approvals.py. Watch: Invoice queue Status flips to Payment Ready + Approved At gets stamped + Slack gets a 'Ready for Payment' message.
- [ ] Confirm the auto-approved row is already at Status=Approved with no further action needed.

> [!CAUTION]
> **Gotchas**
>
> - If the Status flip doesn't trigger the Payment Ready transition, check that process_approvals.py is reading the same DOCK_WORKSPACE_SLUG.
> - If Slack mentions don't notify the approver, the bot user might not be in the channel. Invite the bot to the channel.

## Step 4: Schedule hourly route + daily approval sweep

_Estimated time: 10 min_

Two cron tasks: route_invoices.py hourly (or every 15 min if you process invoices faster), process_approvals.py daily at 9 AM. CueAPI is the right pick for cloud-scheduled runs. Cron is fine for an always-on machine.

### Tasks

- [ ] Option A, cron: crontab -e, add `0 * * * * cd /path && source .env && python3 route_invoices.py >> route.log 2>&1` and `0 9 * * * cd /path && source .env && python3 process_approvals.py >> process.log 2>&1`
- [ ] Option B, CueAPI: pip install cueapi cueapi-worker, then cueapi create --schedule '0 * * * *' --name 'ap-invoice-router' --handler ./route_invoices.py and cueapi create --schedule '0 9 * * *' --name 'ap-invoice-processor' --handler ./process_approvals.py
- [ ] Confirm next hour: Status has a fresh session entry. Drop a real invoice + watch the routing fire automatically.

> [!CAUTION]
> **Gotchas**
>
> - Hourly route is the typical pace. Faster than that and approvers get hit with a Slack ping per invoice within minutes; slower than that and invoices sit in the inbox folder visibly stale.
> - Daily process_approvals.py is enough for most teams. Bump to twice-daily if approvers tend to clear queues mid-afternoon.

## Step 5: Set the audit + month-end review cadence

_Estimated time: 15 min/month ongoing_

Beyond the routing itself, the value of this template is the audit log. Every routing decision lives in Approval log doc + every invoice row stays in Invoice queue. Month-end, walk the queue: how many invoices auto-approved, how many escalated, anything that should change about the thresholds.

### Tasks

- [ ] Block 15 min at month-end on the calendar
- [ ] Open Invoice queue (table). Filter to Submitted At in last 30 days. Count rows at each Status.
- [ ] Are too many invoices going to the high-value approver? Bump HIGH_VALUE_ABOVE.
- [ ] Are auto-approvals catching invoices that should have a human eye? Lower AUTO_APPROVE_UNDER.
- [ ] Walk Approval log (doc) for any rejected invoices. Confirm the vendor follow-up happened.

> [!CAUTION]
> **Gotchas**
>
> - Changing AUTO_APPROVE_UNDER mid-month re-routes future invoices but doesn't re-evaluate past rows. That's correct behavior.
> - Auto-approved invoices still belong in the queue for audit. Don't filter them out of the monthly review.

---

## Hand the template to your agent

Paste the prompt below into your agent's permanent system prompt so the agent reads, writes, and maintains this workspace as you work through the steps.

```text
You are the agent running on the "AP invoice routing and approval" template workspace, connected via MCP at your-org/ap-invoice-routing-approval.

Your job: watch invoices-inbox/, extract invoice fields with Claude, route each invoice to the right approver based on amount thresholds, and create one Invoice queue row per invoice. Never flip Pending rows to Approved. Never move money.

User-loop protocol:
- You propose. The operator decides. Never flip Status past what the routing tier dictates. Never mark Payment Ready.
- Hourly (or "route invoices"): list .txt/.md files in INVOICES_INBOX_DIR. Skip names in processed_invoices.json. For each new file, run Claude extract on the contents.
- Extracted fields: vendor, invoice_number, amount (number), due_date (YYYY-MM-DD), department, gl_code, notes (one-sentence summary).
- Route based on amount: below AUTO_APPROVE_UNDER -> Status=Approved + auto-approved Slack post (no approver tagged). Between AUTO_APPROVE_UNDER and HIGH_VALUE_ABOVE -> Status=Pending + Approver=APPROVER_STANDARD + Slack approval request mentioning APPROVER_STANDARD. Above HIGH_VALUE_ABOVE -> Status=Pending + Approver=APPROVER_HIGH_VALUE + Slack approval request mentioning APPROVER_HIGH_VALUE.
- Add the filename to processed_invoices.json on success. If extraction fails with json.JSONDecodeError, mark processed permanently and log. If a transient error (Dock, Slack, network), do NOT mark processed; retry next run.
- End of every working session, write 1 paragraph to Status: invoices processed, routed standard, routed high-value, auto-approved, anything that failed extraction.

Don't touch:
- Invoice queue.Status (Pending / Approved / Rejected / Payment Ready is the operator's flow).
- Invoice queue.Approved At / Approved By (process_approvals.py + the human approver set those).
- processed_invoices.json transient errors (only permanent failures get marked).

First MCP tool calls:
1. list_surfaces(workspace_slug="ap-invoice-routing-approval")
2. list_rows(workspace_slug="ap-invoice-routing-approval", surface_slug="invoice-queue")
3. get_doc(workspace_slug="ap-invoice-routing-approval", surface_slug="status")
```

---

## FAQ

### Does the agent move money or initiate payments?

No. The agent extracts, routes, and tracks. Payment Ready is the workflow's terminal state. Actual payment happens in your AP system (QuickBooks, Xero, NetSuite, or a bank portal) by a human. The Extending section of Setup guide outlines wiring Payment Ready to a QuickBooks bill creation, but money movement is operator-initiated.

### Can I add PDF invoices to the inbox folder?

Yes, with one extension: pip install pdfplumber, then in route_invoices.py extract text with pdfplumber.open(path) before passing to extract_invoice(). The base template handles .txt and .md only because not every operator wants the pdfplumber dep.

### What if Claude misreads an amount or vendor?

Two protections. First, the approver sees the extracted fields in the Slack message before approving; misreads get caught here. Second, every routing decision logs to Approval log doc, so audit trail is intact. If you see persistent extraction misses on a vendor, edit the prompt in extract_invoice() to include a vendor-specific hint.

### Can I route by department too, not just amount?

Yes, via the routing_rules.json extension in Setup guide. The base template routes on amount only. You can add per-department routing (IT invoices to IT director, marketing invoices to CMO) by extending the routing function. The pattern is the same: read department from the extracted fields, look up the right approver, route.

### What happens to invoices that fail Claude extraction?

They get logged and marked permanently processed (json.JSONDecodeError). The agent moves on; the file stays in the inbox folder for manual review. Transient errors (Dock down, Slack 500) don't mark processed and will retry next run. This is intentional: a bad scan shouldn't loop forever, but a Dock outage shouldn't lose invoices.

