Tutorial // Finance2026-07-1818 min read

Multi-Agent Orchestration in Eve.dev: An Autonomous Accounts-Payable Pipeline

Use Eve.dev's multi-agent orchestration to build an accounts-payable pipeline: a supervisor delegates invoice extraction, 3-way matching, and durable approvals.

Varun Raj Manoharan
Varun Raj Manoharan
Eve.devMulti-AgentAccounts PayableInvoice Automation3-Way MatchingDurable Agents

Key takeaways

  • Accounts payable isn't a chat problem, it's a pipeline: extract the invoice, match it against the purchase order and goods receipt, then route it for payment or review. Each stage wants a different specialist.
  • A supervisor agent that delegates to focused sub-agents beats one giant agent trying to hold the whole process in a single prompt. Eve.dev exposes sub-agent calls as a first-class orchestration primitive.
  • 3-way matching—purchase order, goods-receipt note, and invoice—is the core control. Configurable tolerances decide what auto-approves and what becomes an exception a human has to see.
  • High-value invoices and out-of-tolerance exceptions suspend the pipeline for durable approval that can wait days without losing state.
  • Duplicate and fraud checks belong before payment, keyed on vendor, invoice number, amount, and date—cheap to run, expensive to skip.

Summary

TL;DR: This tutorial builds an autonomous accounts-payable pipeline with Eve.dev using multi-agent orchestration. A supervisor agent receives an incoming invoice and delegates to three specialists: an extractor that reads the document, a 3-way matcher that checks it against the purchase order and goods receipt, and an approval router that either auto-approves within tolerance or suspends—durably—for human sign-off on exceptions and high-value bills. You'll build the sub-agents, the delegation primitive, the matching logic with tolerances, and the durable approval gate.

Ask someone in finance to describe accounts payable and they won't describe a conversation. They'll describe an assembly line: an invoice comes in, someone reads it, someone checks it against what was ordered and what was actually delivered, someone decides whether it can be paid, and—eventually—someone pays it. It's a pipeline with checkpoints, and every checkpoint exists because skipping it once cost a company real money.

That's why a single do-everything AI agent struggles here. Reading a messy PDF invoice, reconciling three documents against each other, and reasoning about approval authority are genuinely different skills. Cram them into one prompt and the agent gets mediocre at all three, and you can't tell which part failed when something goes wrong.

The better fit is orchestration: a supervisor that understands the workflow and hands each step to a specialist built for it. Eve.dev treats that as a first-class capability, and accounts payable is close to the perfect case study for it—structured enough to automate, consequential enough that you want humans on the exceptions.

Why one agent isn't enough

A multi-agent system is one where a coordinating agent delegates subtasks to focused sub-agents, each with its own instructions and tools, instead of a single agent trying to do everything. The argument for it here is practical, not architectural fashion.

Consider what each stage actually needs:

  • Extraction wants a document-savvy agent with OCR and parsing tools, tuned to pull vendor, line items, amounts, and PO references out of layouts that are never quite consistent.
  • Matching wants an agent that reasons about numbers and tolerances and has read access to your purchasing records—not one that's also trying to remember how to parse a PDF.
  • Approval routing wants an agent that understands your authority thresholds and knows when to stop and ask a person.

Give one agent all three jobs and every job's context pollutes the others. Split them, and each sub-agent has a small, testable surface: clear inputs, clear outputs, one responsibility. That's easier to build, easier to debug, and—when an invoice gets mishandled—easier to pin to the stage that dropped it. The supervisor's only job is knowing the order of operations and passing results along.

The pipeline we're building

Here's the flow, end to end:

VBNET
incoming invoice
      │
      ▼
┌─────────────┐   delegate    ┌────────────────────┐
│ AP          │──────────────▶│ invoice-extractor  │  reads the document
│ supervisor  │◀──────────────│                    │
│             │               └────────────────────┘
│             │   delegate    ┌────────────────────┐
│             │──────────────▶│ three-way-matcher  │  PO + GRN + invoice
│             │◀──────────────│                    │
│             │               └────────────────────┘
│             │   delegate    ┌────────────────────┐
│             │──────────────▶│ approval-router    │  auto-approve or suspend
└─────────────┘◀──────────────│                    │
      │                       └────────────────────┘
      ▼
  pay  /  wait for human  /  send back as exception

Before we build it, we need to be precise about the step everyone waves at and few explain: the 3-way match.

How 3-way matching actually works

3-way matching is the core accounts-payable control that validates a vendor invoice by cross-checking it against two other documents before anyone pays it: the purchase order (what you agreed to buy), the goods-receipt note or GRN (what was actually delivered), and the invoice itself (what you're being billed for). The match compares them across quantities, pricing, and line items. Only when all three agree does the invoice clear for payment.

In practice it's a five-step sequence:

  1. Create the purchase order — the buyer commits to quantity and price.
  2. Receive the goods — receiving records what showed up, producing the goods-receipt note.
  3. Receive the invoice — the vendor bills you.
  4. Match all three — quantities, unit prices, and line items are compared across PO, GRN, and invoice.
  5. Approve or escalate — if everything lines up, it's cleared; if not, it becomes an exception for review.

The subtlety that makes automation possible is matching tolerances. Real invoices rarely match to the penny—rounding, freight, minor price drift. So you configure acceptable variance thresholds: a small percentage or a flat amount, applied to quantity, unit price, or extended amount. A common shape is something like "within 1% or $5, auto-approve." Tighter programs run 1–2%, looser ones 3–5%. Anything inside the tolerance passes untouched—that's what "touchless" or straight-through processing means. Anything outside it becomes an exception a human has to look at. ERP systems like Microsoft Dynamics and NetSuite implement exactly this: validate against criteria, auto-approve within tolerance, route the rest for approval.

Our approval router will encode precisely this logic.

Project setup

Shell
npx create-next-app@latest ap-pipeline
cd ap-pipeline
npm install @eve/core

Eve.dev's filesystem-first layout means each agent—supervisor and specialists alike—is just a folder under eve/agents/. Create them:

Shell
mkdir -p eve/agents/ap-supervisor/tools
mkdir -p eve/agents/invoice-extractor/tools
mkdir -p eve/agents/three-way-matcher/tools
mkdir -p eve/agents/approval-router/tools

The full structure:

Shell
eve/
└── agents/
    ├── ap-supervisor/         # the orchestrator
    │   ├── instructions.md
    │   └── tools/
    │       └── delegate.ts    # calls the specialists
    ├── invoice-extractor/
    │   ├── instructions.md
    │   └── tools/extract.ts
    ├── three-way-matcher/
    │   ├── instructions.md
    │   └── tools/{fetch_po.ts, fetch_grn.ts}
    └── approval-router/
        ├── instructions.md
        └── tools/request_signoff.ts

Each specialist is a complete, standalone agent. The supervisor is what ties them into a pipeline.

Step 1: Build the specialist sub-agents

Start with the extractor. Its eve/agents/invoice-extractor/instructions.md:

MARKDOWN
# Invoice Extractor

You read a single vendor invoice (PDF, image, or email body) and return
structured data. Extract: vendor name, invoice number, invoice date, PO
number if present, currency, line items (description, quantity, unit price),
subtotal, tax, and total.

Return ONLY the structured fields. Do not judge or approve anything —
that is not your job. If a field is missing or unreadable, mark it null;
never guess an amount.

Its one tool, eve/agents/invoice-extractor/tools/extract.ts:

TypeScript
import { tool } from '@eve/core';
import { z } from 'zod';

export default tool({
  name: 'extract',
  description: 'OCR and parse a raw invoice document into structured fields.',
  schema: z.object({ documentUrl: z.string() }),
  execute: async ({ documentUrl }) => {
    // Call your OCR/document-AI provider here.
    return {
      vendor: 'Acme Supplies',
      invoiceNumber: 'INV-4471',
      invoiceDate: '2026-07-10',
      poNumber: 'PO-2201',
      currency: 'USD',
      lineItems: [{ description: 'Widget A', qty: 100, unitPrice: 4.50 }],
      subtotal: 450.0,
      tax: 36.0,
      total: 486.0,
    };
  },
});

The matcher gets read access to purchasing records. Its eve/agents/three-way-matcher/instructions.md:

MARKDOWN
# Three-Way Matcher

You receive extracted invoice data. Fetch the matching purchase order and
goods-receipt note, then compare all three across quantity, unit price, and
line total. Report the variance on each dimension as both an absolute amount
and a percentage. Do not decide whether to approve — just report the deltas
and whether a PO and GRN could be found at all.

And its lookup tools—eve/agents/three-way-matcher/tools/fetch_po.ts (the GRN tool mirrors it):

TypeScript
import { tool } from '@eve/core';
import { z } from 'zod';

export default tool({
  name: 'fetch_po',
  description: 'Look up a purchase order by number from the ERP. Read-only.',
  schema: z.object({ poNumber: z.string() }),
  execute: async ({ poNumber }) => {
    // Read from your ERP (NetSuite, SAP, Dynamics, etc.)
    return {
      poNumber,
      lineItems: [{ description: 'Widget A', qty: 100, unitPrice: 4.50 }],
      total: 450.0,
    };
  },
});

Notice the recurring discipline: specialists report, they don't decide. The extractor extracts, the matcher measures variance. Neither one approves a payment. That authority lives in exactly one place, which is what makes the whole pipeline auditable.

Step 2: The supervisor that delegates

Now the orchestration. The supervisor's eve/agents/ap-supervisor/instructions.md:

MARKDOWN
# Accounts-Payable Supervisor

You process one incoming invoice from receipt to a payment decision. Run the
pipeline in order and never skip a step:

1. Delegate to `invoice-extractor` to read the document.
2. Delegate to `three-way-matcher` with the extracted data to get variances.
3. Delegate to `approval-router` with the invoice and the match result to get
   the final decision (auto-approved, needs human sign-off, or exception).

You coordinate. You do not read documents, compute matches, or approve
payments yourself — each specialist owns its step. Pass results forward
faithfully and return the final decision.

The delegation itself is the interesting part. Eve.dev injects a runtime context into every tool as a second argument, and that context can start a sub-agent as a durable child run. Create eve/agents/ap-supervisor/tools/delegate.ts:

TypeScript
import { tool } from '@eve/core';
import { z } from 'zod';

export default tool({
  name: 'delegate',
  description: 'Hand a subtask to a specialist sub-agent and return its structured result.',
  schema: z.object({
    agent: z.enum(['invoice-extractor', 'three-way-matcher', 'approval-router']),
    input: z.record(z.any()),
  }),
  execute: async ({ agent, input }, ctx) => {
    // ctx.runAgent starts the named sub-agent as a durable child of this run.
    // If the child suspends (e.g. waiting for approval), the parent suspends
    // with it — the whole pipeline is one durable workflow.
    const result = await ctx.runAgent(agent, { input });
    return result.output;
  },
});

That second parameter, ctx, is what makes this multi-agent rather than one big agent. ctx.runAgent runs a specialist as a durable child of the supervisor's workflow. The results flow back up, and—crucially—if a child suspends, the parent suspends too. The entire pipeline behaves as a single checkpointed workflow even though it's assembled from four independent agents. That property is the whole reason to reach for orchestration here instead of gluing HTTP calls together yourself.

Step 3: Encode tolerances and exceptions

The approval router is where policy lives. It takes the invoice and the matcher's variance report and applies your tolerance rules. Its eve/agents/approval-router/tools/request_signoff.ts handles the decision and, when needed, the durable wait:

TypeScript
import { tool, suspend } from '@eve/core';
import { z } from 'zod';

// Your finance policy, in one place.
const TOLERANCE = { pct: 0.01, amount: 5 };      // within 1% OR $5 -> touchless
const AUTO_APPROVE_CEILING = 2500;                // above this, always a human

export default tool({
  name: 'request_signoff',
  description: 'Apply tolerance + authority rules. Auto-approve, or suspend for human sign-off.',
  schema: z.object({
    invoiceNumber: z.string(),
    total: z.number(),
    maxVariancePct: z.number(),   // worst line variance from the matcher
    maxVarianceAmount: z.number(),
    poFound: z.boolean(),
    grnFound: z.boolean(),
  }),
  execute: async (v) => {
    const withinTolerance =
      v.maxVariancePct <= TOLERANCE.pct || v.maxVarianceAmount <= TOLERANCE.amount;

    // An exception is anything that can't clear straight through.
    const exception =
      !v.poFound ? 'missing_po'
      : !v.grnFound ? 'missing_receipt'
      : !withinTolerance ? 'price_or_qty_mismatch'
      : v.total > AUTO_APPROVE_CEILING ? 'over_ceiling'
      : null;

    if (!exception) {
      return { decision: 'auto_approved', invoiceNumber: v.invoiceNumber };
    }

    // Needs a human. Park until someone with authority signs off.
    return suspend({
      reason: 'awaiting_signoff',
      message: `Invoice ${v.invoiceNumber} ($${v.total}) needs review: ${exception}`,
      data: { invoiceNumber: v.invoiceNumber, exception },
    });
  },
});

Two knobs run the whole thing. TOLERANCE decides what's close enough to pay without a human. AUTO_APPROVE_CEILING enforces authority—no matter how clean the match, a large invoice gets a person. Everything that can't clear becomes a named exception. Those categories aren't arbitrary; they mirror the ones AP teams actually track: PO mismatches, price or quantity discrepancies, missing data, missing receipt or approval, and duplicates. Naming the exception is what lets you route it to the right person and report on where your invoices get stuck.

Step 4: Durable approval that can wait days

Here's where durability pays off in AP specifically. An invoice that trips the ceiling might wait for a department head who's on vacation. That approval could genuinely take a week. A stateless system would either time out or pin a request open for days.

Because request_signoff uses suspend(), the invoice parks in durable storage and consumes nothing while it waits. The supervisor's delegate call to the approval router is suspended right along with it—remember, a suspended child suspends the parent. Days later, when the approver clicks yes, the entire pipeline wakes up from its checkpoint and finishes: record the approval, hand off to payment, done. The same durable wait-state mechanics we use for support and on-call agents map cleanly onto AP, because "wait for a human, however long it takes, without losing state" is the universal shape.

Step 5: Duplicate and fraud checks before payment

Paying the same invoice twice is one of the most common—and most avoidable—ways AP leaks money. Duplicate detection doesn't need document comparison; it works by matching incoming invoices against your history on a small set of fields: vendor, invoice number, amount, and date. Run it as a gate the supervisor calls before any payment clears. Add eve/agents/ap-supervisor/tools/duplicate_check.ts:

TypeScript
import { tool } from '@eve/core';
import { z } from 'zod';

export default tool({
  name: 'duplicate_check',
  description: 'Flag a likely duplicate by matching against historical invoices on vendor, number, amount, and date.',
  schema: z.object({
    vendor: z.string(),
    invoiceNumber: z.string(),
    total: z.number(),
    invoiceDate: z.string(),
  }),
  execute: async (inv) => {
    // Query indexed historical invoices for an exact/near match.
    const match = await findSimilarInvoice(inv);
    return { isDuplicate: Boolean(match), matchedId: match?.id ?? null };
  },
});

Fold it into the supervisor's instructions as a mandatory pre-payment step: a flagged duplicate never auto-approves—it becomes an exception, full stop. It's a cheap check that catches an expensive mistake, which is exactly the kind of guardrail you want running on every single invoice.

Step 6: Run the pipeline

Finally, the entry point. Invoices usually arrive by email or a vendor portal; a webhook kicks off the supervisor. Create app/api/invoice/route.ts:

TypeScript
import { Eve } from '@eve/core';
import { NextResponse } from 'next/server';

const eve = new Eve({
  workspace: './eve',
  storage: process.env.EVE_STORAGE_URL,
});

export async function POST(req: Request) {
  const event = await req.json();
  const supervisor = await eve.getAgent('ap-supervisor');

  // One durable workflow per invoice.
  const result = await supervisor.run({
    id: `invoice:${event.invoiceId}`,
    input: event.signoff ? undefined : { documentUrl: event.documentUrl },
    resume: event.signoff, // { invoiceNumber, approved, approver } or undefined
  });

  if (result.status === 'suspended') {
    return NextResponse.json({ state: 'awaiting_signoff', message: result.suspensionMessage });
  }
  return NextResponse.json({ state: 'complete', decision: result.finalOutput });
}

Same durable-id pattern as every other Eve.dev build: a new invoiceId starts the pipeline, and an approval callback resumes the exact same workflow from its checkpoint. Your finance team's "approve" button in Slack or your ERP just POSTs back here with a signoff payload.

Handling the messy reality

Clean invoices are the easy 70%. A production AP agent is judged on the rest:

Non-PO invoices are the hard case. A lot of spend—utilities, subscriptions, one-off services—never had a purchase order, so there's nothing to 3-way match against. Route these down a different path: your three-way-matcher should report "no PO," and the approval router should treat non-PO invoices as needing human eyes by default until you've built up rules for the recurring ones. Don't pretend a match happened when it didn't.

Your tolerances are a policy dial, not a constant. Set them too tight and everything becomes an exception, drowning your team and defeating the point. Too loose and you rubber-stamp real errors. Start conservative, watch which invoices land in the exception queue, and loosen deliberately. The tolerance config living in one file makes that a code review, not an archaeology project.

Keep the ERP as the source of truth. The agent decides; your ERP records. Every approval and payment decision should write back to NetSuite, SAP, or whatever you run, and the agent should read PO and receipt data from there rather than a copy. The pipeline is orchestration on top of your system of record, not a replacement for it.

Test each specialist in isolation. The payoff of splitting into sub-agents is that you can. Feed the extractor a stack of ugly real invoices and check the fields. Feed the matcher known PO/GRN/invoice triples and check the variances. Because each agent has one job and a clean interface, its tests are small and its failures are obvious.

Where this fits with the rest of your agents

This build combines the two Eve.dev capabilities that come up again and again, pointed at a workflow where correctness is money:

  • Multi-agent orchestration via ctx.runAgent is the new piece—a supervisor coordinating specialists as durable child workflows.
  • The durable suspend() approval gate is the same one behind the human-in-the-loop guide and the durable support agent.
  • The file-per-agent, file-per-tool layout is the filesystem-first architecture doing what it's good at: keeping a four-agent system legible.

The result is a pipeline that clears the clean invoices on its own, stops and asks a human on anything ambiguous or expensive, and never pays the same bill twice.

Frequently Asked Questions

What is 3-way matching in accounts payable? It's the control that validates a vendor invoice against two other documents before payment: the purchase order (what you agreed to buy), the goods-receipt note (what was actually delivered), and the invoice (what you're billed). Quantities, unit prices, and line items are compared across all three. If they agree within tolerance, the invoice clears; if not, it becomes an exception for review. The full sequence is: create the PO, receive the goods, receive the invoice, match all three, then approve or escalate.

Why use multiple agents instead of one for invoice processing? Because the stages need different skills. Reading a messy PDF, reconciling three documents, and applying approval authority are separate problems, and one agent doing all three gets mediocre at each while making failures hard to localize. A supervisor delegating to focused specialists gives each a small, testable surface and puts payment authority in exactly one place, which is what makes the pipeline auditable.

How does the pipeline handle invoice exceptions? Anything that can't clear straight through becomes a named exception—missing PO, missing receipt, a price or quantity mismatch outside tolerance, a suspected duplicate, or an amount over the auto-approve ceiling. Exceptions never auto-approve; they suspend the workflow for a human. Because Eve.dev checkpoints the state, that pause can last minutes or days without losing context, and the pipeline resumes from where it stopped once someone signs off.

What are matching tolerances and why do they matter? Tolerances are the acceptable variance thresholds—a small percentage or flat amount—that let minor discrepancies pass without manual review. Real invoices rarely match to the penny because of rounding, freight, or small price drift, so a rule like "within 1% or $5, auto-approve" is what makes touchless processing possible. Set them too tight and everything becomes an exception; too loose and you approve real errors. They're a policy dial you tune over time.

Can AI detect duplicate or fraudulent invoices? Duplicate detection works well by matching an incoming invoice against your history on vendor, invoice number, amount, and date—no document comparison needed—run as a gate before payment. In this build a flagged duplicate is forced into the exception path rather than auto-approved. It won't catch every sophisticated fraud on its own, but running the check on every invoice removes the most common and expensive mistake, which is simply paying the same bill twice.

What is touchless (straight-through) invoice processing? It's an invoice that flows from receipt to payment-ready without a human touching it—extracted, matched within tolerance, under the approval ceiling, and not a duplicate. The realistic share that goes fully touchless varies a lot by organization and depends heavily on how many of your invoices are PO-backed, since non-PO invoices almost always need a human. The goal isn't 100% touchless; it's to automate the clean majority and route human attention to the cases that actually need judgment.

Does this work with NetSuite, SAP, or Dynamics? Yes. The specialists read PO and goods-receipt data from your ERP through their lookup tools, and the pipeline writes approval and payment decisions back to it. The ERP stays the system of record; the Eve.dev pipeline is the orchestration layer on top. Swapping ERPs means changing the tool implementations, not the pipeline structure.

Available for new projects

Let's build something great.

Have a project in mind? We are an elite software and AI development studio ready to bring your ideas to production. Let's talk about your roadmap.