Guide

Build an Invoice-Intake Agent

Vendor invoices arrive as PDF attachments in a shared inbox, and someone retypes them into the accounting system one at a time. An agent account automates the first mile. This guide builds an AP-intake agent on a dedicated inbox: it reads each inbound message, finds the invoice attachment by content type, downloads it, hands the file to a parser that extracts vendor, amount, and due date, and routes a structured summary to accounting — with guardrails so an untrusted attachment can't turn the agent into an exfiltration tool.

Written by Pouya Sanooei Software Engineer

VerifiedCLI 3.1.16 · Nylas managed · last tested June 7, 2026

What is an invoice-intake agent?

An invoice-intake agent is an autonomous program that turns an emailed invoice into a structured record for accounting. It watches a dedicated inbox, identifies the invoice attachment on each incoming message, downloads it, extracts the fields a payables team needs — vendor, invoice number, amount, due date — and forwards a clean summary to the right person or system. Built on an agent account, the inbox and the processing both live on one managed identity.

The job exists because the first mile of accounts payable is manual: a human opens a PDF, reads four numbers, and retypes them into a system. That step is repetitive, error-prone, and slow, which makes it a clean fit for automation — the model reads the document, and deterministic code moves the data. The agent handles the volume; a person reviews the exceptions.

Invoice-intake flow: read the message, download the PDF attachment, extract fields, route a summary to accountingRead invoiceemail listDownload PDFattachmentsExtractmodel parseRoute to APemail send

Why run AP intake on an agent account?

An AP-intake agent should own a dedicated inbox like invoices@, not share a person's mailbox. On an agent account the inbox is the agent's own, the downloaded files and the routing are attributable to the agent, and you can stop the pipeline by suspending one grant. Vendors send to one stable address, and everything that happens to those messages is automated and logged.

Containment matters more here than usual because the agent opens attachments from outside parties. A leads inbox handles untrusted text; an invoice inbox handles untrusted files — a fuller lethal trifecta of private data, untrusted content, and outbound email. Because the agent account's outbound rules live on the workspace, a malicious attachment can't prompt the agent into forwarding data anywhere but accounting — containment lives outside the agent's decision loop.

How do I set up the invoices inbox?

Create the intake identity with one command. The nylas agent account create call returns a grant in under 2 seconds, and the address is ready to receive invoices immediately. Publish it to your vendors as the billing contact:

nylas agent account create invoices@yourapp.nylas.email
export NYLAS_GRANT_ID="$(nylas agent account get invoices@yourapp.nylas.email --quiet)"

Every vendor now sends to one address the agent owns. Because the inbox is dedicated to invoices, the agent's job is narrow and its rules are simple — a focused inbox is easier to automate safely than a general one where invoices mix with everything else.

How does the agent find invoice attachments?

The agent reads unread messages, then inspects each one's attachments. The nylas email attachments list command returns every attachment on a message with its id, filename, size, and content_type. Filter on the content type — invoices are almost always application/pdf, a MIME media type:

msg_id=$(nylas email list --unread --json | jq -re '.[0].id')

# Find the PDF attachment id on that message
nylas email attachments list "$msg_id" --json \
  | jq -r '.[] | select(.content_type == "application/pdf") | .id'

Filtering on content_type rather than the filename extension is the better first-pass filter — a file named invoice.pdf whose declared MIME type isn't PDF gets skipped. This checks the API's reported content type, not the file's bytes, so validate the downloaded file before trusting it. A message with no PDF attachment is a non-invoice and should be routed to a human, not processed.

How does the agent download and extract invoice data?

With the attachment id, the agent downloads the file to a working directory with nylas email attachments download. The command takes the attachment id and the message id, plus an --output path. Once the PDF is local, a parser or a vision model extracts the structured fields:

mkdir -p ./invoices

att_id=$(nylas email attachments list "$msg_id" --json \
  | jq -r 'first(.[] | select(.content_type == "application/pdf") | .id) // empty')

nylas email attachments download "$att_id" "$msg_id" \
  --output "./invoices/$msg_id.pdf"

Extraction is the model's job: read the PDF and return vendor, invoice number, amount, currency, and due date as JSON. Keep the file in a scratch directory the agent can't execute from, and never run anything inside the attachment — a PDF is data to read, not code to run. The deterministic code takes the model's structured output and moves it; the model never touches an email command directly.

How does the agent route the invoice to accounting?

A parsed invoice becomes a structured summary sent to accounting. The nylas email send command delivers it to a fixed AP address — never a recipient derived from the invoice itself, which is the line that stops a spoofed invoice from redirecting payment. Put the extracted fields in the body so a person or an automation can act without reopening the PDF:

nylas email send \
  --to ap@yourcompany.com \
  --subject "Invoice: Acme Corp #INV-4821, USD 2,400 due 2026-07-01" \
  --body "Vendor: Acme Corp
Invoice: INV-4821
Amount: USD 2400.00
Due: 2026-07-01
Source message: $msg_id"

Hard-coding the AP recipient is a deliberate control, not a shortcut. An invoice is an instruction from an outside party, and a common fraud is an invoice that asks you to "update payment details" — if the agent derived its recipient or payment target from the document, that attack would work. Sending only to a fixed internal address removes the option entirely.

How do I run the intake loop?

The full loop runs on a cron schedule. It reads the oldest unread message, finds a PDF, downloads it, extracts the fields, routes the summary, and marks the message read so it isn't processed twice. Messages with no PDF go to a human instead of being dropped. The whole thing runs under set -euo pipefail:

#!/usr/bin/env bash
set -euo pipefail

msg=$(nylas email list --unread --json | jq -r '.[0] // empty')
[ -z "$msg" ] && exit 0
msg_id=$(echo "$msg" | jq -re '.id')
sender=$(echo "$msg" | jq -r '.from[0].email')

# Find a PDF attachment; if none, hand the message to a human
att_id=$(nylas email attachments list "$msg_id" --json \
  | jq -r 'first(.[] | select(.content_type == "application/pdf") | .id) // empty')

if [ -z "$att_id" ]; then
  nylas email send --to ap@yourcompany.com \
    --subject "Non-invoice message from $sender — please review" \
    --body "No PDF attachment found. Source message: $msg_id"
  nylas email mark read "$msg_id"
  exit 0
fi

# Download and extract (extract_invoice is your parser/model call)
mkdir -p ./invoices
nylas email attachments download "$att_id" "$msg_id" --output "./invoices/$msg_id.pdf"
fields=$(extract_invoice "./invoices/$msg_id.pdf")

# Route a structured summary to a FIXED AP address only
nylas email send --to ap@yourcompany.com \
  --subject "Invoice from $sender" \
  --body "$fields"$'\n'"Source message: $msg_id"

nylas email mark read "$msg_id"

The first(...) jq filter takes the first PDF when a message has several, and the no-attachment branch forwards to a human rather than silently skipping. Marking the message read on every path that completes keeps the next run from reprocessing it; a failure during download or send aborts under set -e before the mark, so a broken invoice stays unread for the next run to retry. Add audit logging around the extraction and send so every routed invoice is traceable — the audit guide covers structured logging for agent actions.

How do I keep the intake agent safe?

An invoice inbox is a high-value target, so containment is part of the design, not an add-on. The primary control is the fixed AP recipient covered above — the agent only ever sends to a hard-coded internal address. The outbound rule is supplemental: it blocks specific known-bad recipient domains a crafted invoice might try to steer a reply toward, evaluated before the send pipeline so the agent can't get a message out to them:

# The agent only ever emails internal accounting — block a flagged exfil domain
nylas agent rule create \
  --name "Block flagged invoice-agent outbound" \
  --trigger outbound \
  --condition recipient.domain,is,exfil.example \
  --action block

Pair the rule with a policy send cap and an attachment-size limit so a malformed message can't exhaust the pipeline, and keep downloaded files in a non-executable scratch directory. The full set of triggers, conditions, and policy limits is in Agent Rules and Policies, and the broader containment pattern is in Stop Your AI Agent From Going Rogue.

Next steps