Skip to content
← All templates

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.

llm_callhuman_reviewtry_catchfor_eachactivepieces

What this workflow does

01

Receive invoice email Gmail trigger watches a dedicated inbox (invoices@yourcompany.com). Extracts sender, subject, body text, and attachment URLs.

02

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.

03

Validation Checks that total matches sum of line items, currency is supported, and vendor exists in your system. Flags discrepancies.

04

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.

05

Post to accounting Creates a bill in QuickBooks (or Xero, FreshBooks) via Activepieces connector. Includes all line items, vendor mapping, and due date.

06

Notification Sends a Slack confirmation to #finance with invoice summary and accounting system link.

07

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 notifications
quickbooks-oauthoauth2QuickBooks OAuth2 token. Swap to xero or freshbooks connector.
internal-apiapi_keyYour internal API for vendor validation

How 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.