WorkflowMedium

Invoice Approval Workflow

Multi-stage approval process with email notifications, OCR, and ERP sync.

The Challenge

Invoices arrive via email from various vendors. They need to be:

  1. Extracted: Key data (Amount, Date, Vendor) needs to be parsed from PDF attachments.
  2. Validated: Checked against purchase orders or budget limits.
  3. Approved: Sent to the correct cost center manager for approval.
  4. Booked: Uploaded to the accounting system (DATEV) with the correct metadata.

Manual processing is slow, error-prone, and lacks visibility.

The Solution

A stateful workflow that orchestrates the entire process, combining AI for extraction with human oversight for approval.

Workflow Definition

import { defineWorkflow } from '@dataflows/core'
import { datev } from '@dataflows/datev'
import { email } from '@dataflows/email'
import { openai } from '@dataflows/openai'
import { slack } from '@dataflows/slack'

export const invoiceApproval = defineWorkflow({
  id: 'invoice-approval',
  trigger: email.onReceive({ subject: 'Invoice' }),
  
  async run({ event, step }) {
    // 1. Extract PDF
    const pdf = await step.run('extract-pdf', () => {
      const attachment = event.attachments.find(a => a.type === 'application/pdf')
      if (!attachment) throw new Error('No PDF found')
      return attachment
    })

    // 2. AI Data Extraction
    const invoiceData = await step.run('ai-extract', async () => {
      return openai.extractStructuredData({
        file: pdf,
        schema: z.object({
          amount: z.number(),
          date: z.string(),
          vendor: z.string(),
          costCenter: z.string()
        })
      })
    })

    // 3. Determine Approver
    const approverEmail = await step.run('get-approver', async () => {
      // Logic to find approver based on cost center
      return `manager-${invoiceData.costCenter}@company.com`
    })

    // 4. Request Approval
    const approval = await step.waitForEvent('approval', {
      timeout: '7d',
      action: async () => {
        await email.send({
          to: approverEmail,
          subject: `Approve Invoice from ${invoiceData.vendor}`,
          body: `
            Amount: ${invoiceData.amount}
            Vendor: ${invoiceData.vendor}
            
            Click here to approve: ${step.callbackUrl({ decision: 'approved' })}
            Click here to reject: ${step.callbackUrl({ decision: 'rejected' })}
          `
        })
        
        await slack.postMessage({
          channel: '#finance-alerts',
          text: `New invoice waiting for approval from ${approverEmail}`
        })
      }
    })

    if (approval.payload.decision === 'rejected') {
      await email.send({ to: event.from, subject: 'Invoice Rejected', body: '...' })
      return { status: 'rejected' }
    }

    // 5. Upload to DATEV
    await step.run('upload-datev', async () => {
      await datev.uploadDocument({
        file: pdf,
        metadata: invoiceData
      })
    })
    
    return { status: 'processed', id: invoiceData.id }
  }
})

Features

Human-in-the-loop

Pauses execution until a human approves via email link.

AI Extraction

Uses OpenAI to extract invoice data (Amount, Date, Vendor) from PDF.

State Persistence

Workflow state is persisted in DB, surviving server restarts.

Tech Stack

DATEVSendGridSlackOpenAI