Invoices arrive via email from various vendors. They need to be:
Manual processing is slow, error-prone, and lacks visibility.
A stateful workflow that orchestrates the entire process, combining AI for extraction with human oversight for approval.
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 }
}
})
Pauses execution until a human approves via email link.
Uses OpenAI to extract invoice data (Amount, Date, Vendor) from PDF.
Workflow state is persisted in DB, surviving server restarts.