Invoice Processing
Email trigger receives an invoice, LLM extracts vendor, line items, and totals, human approves amounts over your threshold, and the result posts to your accounting system. Retry with backoff on every external call.
What this workflow does
Receive invoice email — Gmail trigger watches a dedicated inbox (invoices@yourcompany.com). Extracts sender, subject, body text, and attachment URLs.
LLM extraction — Sends the email body (and attachment text if available) to an LLM. Returns structured JSON: vendor name, invoice number, line items with amounts, total, currency, due date.
Validation — Checks that total matches sum of line items, currency is supported, and vendor exists in your system. Flags discrepancies.
Approval gate — If total exceeds your threshold (default $5,000), routes to human review. Reviewer sees extracted data and can approve, reject, or edit amounts. 24-hour deadline.
Post to accounting — Creates a bill in QuickBooks (or Xero, FreshBooks) via Activepieces connector. Includes all line items, vendor mapping, and due date.
Notification — Sends a Slack confirmation to #finance with invoice summary and accounting system link.
Error recovery — The entire flow is wrapped in TryCatch. On failure, the invoice lands in the DLQ with full context for manual retry.
Workflow definition
Copy this JSON and POST it to /sequences.
{
"id": "invoice_processing_v1",
"context_schema": {
"from_email": "string",
"subject": "string",
"body": "string",
"attachment_url": "string | null"
},
"blocks": [
{
"id": "extract",
"type": "try_catch",
"try_block": [
{
"id": "llm_extract",
"type": "step",
"handler": "llm_call",
"params": {
"provider": "openai",
"model": "gpt-4o",
"api_key_env": "OPENAI_API_KEY",
"system": "Extract invoice data. Return JSON: { vendor, invoice_number, line_items: [{ description, amount, quantity }], total, currency, due_date }. If data is missing, set field to null.",
"messages": [
{
"role": "user",
"content": "From: {{context.data.from_email}}\nSubject: {{context.data.subject}}\n\n{{context.data.body}}"
}
]
},
"retry": { "max_attempts": 3, "initial_backoff": 1000, "max_backoff": 10000, "backoff_multiplier": 2.0 }
}
],
"catch_block": [
{
"id": "extraction_failed",
"type": "step",
"handler": "ap://slack.send_channel_message",
"params": {
"auth": { "access_token": "credentials://slack-bot" },
"props": {
"channel": "#finance",
"text": "Failed to extract invoice from {{context.data.from_email}}: {{context.data.subject}}. Manual processing needed."
}
}
}
]
},
{
"id": "validate",
"type": "step",
"handler": "http_request",
"params": {
"method": "POST",
"url": "https://api.yourapp.com/invoices/validate",
"headers": { "Authorization": "Bearer credentials://internal-api" },
"body": {
"vendor": "{{steps.llm_extract.output.parsed.vendor}}",
"total": "{{steps.llm_extract.output.parsed.total}}",
"line_items": "{{steps.llm_extract.output.parsed.line_items}}",
"currency": "{{steps.llm_extract.output.parsed.currency}}"
}
}
},
{
"id": "approval_gate",
"type": "router",
"routes": [
{
"condition": "steps.llm_extract.output.parsed.total > 5000",
"blocks": [
{
"id": "human_approval",
"type": "step",
"handler": "human_review",
"params": {
"review_data": "Invoice #{{steps.llm_extract.output.parsed.invoice_number}} from {{steps.llm_extract.output.parsed.vendor}} for {{steps.llm_extract.output.parsed.currency}} {{steps.llm_extract.output.parsed.total}}"
},
"wait_for_input": {
"prompt": "Review invoice and approve, reject, or flag for review.",
"choices": [
{ "label": "Approve", "value": "approved" },
{ "label": "Reject", "value": "rejected" },
{ "label": "Flag for review", "value": "flagged" }
],
"store_as": "approval_decision",
"timeout": 86400000
}
}
]
}
]
},
{
"id": "check_rejection",
"type": "router",
"routes": [
{
"condition": "context.data.approval_decision == 'rejected'",
"blocks": [
{
"id": "notify_rejection",
"type": "step",
"handler": "ap://slack.send_channel_message",
"params": {
"auth": { "access_token": "credentials://slack-bot" },
"props": {
"channel": "#finance",
"text": "Invoice #{{steps.llm_extract.output.parsed.invoice_number}} from {{steps.llm_extract.output.parsed.vendor}} was rejected."
}
}
}
]
}
],
"default": [
{
"id": "post_to_accounting",
"type": "step",
"handler": "ap://quickbooks.create_bill",
"params": {
"auth": { "access_token": "credentials://quickbooks-oauth" },
"props": {
"vendor_name": "{{steps.llm_extract.output.parsed.vendor}}",
"line_items": "{{steps.llm_extract.output.parsed.line_items}}",
"total": "{{steps.llm_extract.output.parsed.total}}",
"due_date": "{{steps.llm_extract.output.parsed.due_date}}"
}
},
"retry": { "max_attempts": 3, "initial_backoff": 1000, "max_backoff": 10000, "backoff_multiplier": 2.0 }
}
]
},
{
"id": "confirm",
"type": "step",
"handler": "ap://slack.send_channel_message",
"params": {
"auth": { "access_token": "credentials://slack-bot" },
"props": {
"channel": "#finance",
"text": "Invoice #{{steps.llm_extract.output.parsed.invoice_number}} processed. {{steps.llm_extract.output.parsed.vendor}} — {{steps.llm_extract.output.parsed.currency}} {{steps.llm_extract.output.parsed.total}}. Posted to accounting."
}
}
}
]
}Credentials to configure
openaiapi_keyOpenAI API key for GPT-4o extraction. Swap to anthropic for Claude.slack-botoauth2Slack bot token with chat:write for #finance notificationsquickbooks-oauthoauth2QuickBooks OAuth2 token. Swap to xero or freshbooks connector.internal-apiapi_keyYour internal API for vendor validationHow to trigger
Option A: Gmail trigger watches an inbox. Option B: Webhook from your email processing service.
// Option A: Watch a Gmail inbox
// POST /triggers
{
"type": "webhook",
"sequence_id": "invoice_processing_v1",
"config": {
"path": "/hooks/invoice-email"
},
"context_mapping": {
"from_email": "body.from",
"subject": "body.subject",
"body": "body.text",
"attachment_url": "body.attachments[0].url"
}
}// Option B: Direct API call
// POST /instances
{
"sequence_id": "invoice_processing_v1",
"context": {
"from_email": "vendor@supplier.com",
"subject": "Invoice #INV-2024-0847",
"body": "Please find attached invoice for Q4 services...",
"attachment_url": null
},
"idempotency_key": "invoice:INV-2024-0847"
}Customize it
Change approval threshold — edit the router condition from total > 5000 to any amount. Or remove the gate entirely for auto-processing.
Add OCR — for PDF attachments, add an OCR step before LLM extraction using an http_request to a document parsing API (AWS Textract, Google Document AI).
Swap accounting system — replace ap://quickbooks.create_bill with ap://xero.create_bill or ap://freshbooks.create_expense.
Duplicate detection — add a step before extraction that checks your accounting system for an existing invoice with the same number from the same vendor.
Multi-currency — add an exchange rate lookup step and convert to your base currency before posting.